ghostrail 0.14.0 → 0.15.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/README.md +2 -2
  2. package/dist/board/server.d.ts +9 -2
  3. package/dist/board/server.d.ts.map +1 -1
  4. package/dist/board/server.js +10 -3
  5. package/dist/board/server.js.map +1 -1
  6. package/dist/cli/commands.d.ts +1 -1
  7. package/dist/cli/commands.d.ts.map +1 -1
  8. package/dist/cli/commands.js +149 -13
  9. package/dist/cli/commands.js.map +1 -1
  10. package/dist/commands/help.js +3 -3
  11. package/dist/commands/help.js.map +1 -1
  12. package/dist/config/load.d.ts.map +1 -1
  13. package/dist/config/load.js +7 -5
  14. package/dist/config/load.js.map +1 -1
  15. package/dist/config/schema.d.ts +15 -0
  16. package/dist/config/schema.d.ts.map +1 -1
  17. package/dist/config/schema.js +5 -0
  18. package/dist/config/schema.js.map +1 -1
  19. package/dist/dashboard/server.d.ts +2 -2
  20. package/dist/dashboard/server.d.ts.map +1 -1
  21. package/dist/dashboard/server.js +8 -4
  22. package/dist/dashboard/server.js.map +1 -1
  23. package/dist/factory/build.d.ts +22 -2
  24. package/dist/factory/build.d.ts.map +1 -1
  25. package/dist/factory/build.js +11 -6
  26. package/dist/factory/build.js.map +1 -1
  27. package/dist/factory/checkout.d.ts +41 -0
  28. package/dist/factory/checkout.d.ts.map +1 -0
  29. package/dist/factory/checkout.js +90 -0
  30. package/dist/factory/checkout.js.map +1 -0
  31. package/dist/factory/state.d.ts +85 -0
  32. package/dist/factory/state.d.ts.map +1 -0
  33. package/dist/factory/state.js +157 -0
  34. package/dist/factory/state.js.map +1 -0
  35. package/dist/global/index.d.ts +3 -1
  36. package/dist/global/index.d.ts.map +1 -1
  37. package/dist/global/index.js +3 -1
  38. package/dist/global/index.js.map +1 -1
  39. package/dist/global/registry.d.ts +38 -5
  40. package/dist/global/registry.d.ts.map +1 -1
  41. package/dist/global/registry.js +40 -14
  42. package/dist/global/registry.js.map +1 -1
  43. package/dist/global/repo-id.d.ts +50 -0
  44. package/dist/global/repo-id.d.ts.map +1 -0
  45. package/dist/global/repo-id.js +149 -0
  46. package/dist/global/repo-id.js.map +1 -0
  47. package/dist/global/state-dir.d.ts +32 -0
  48. package/dist/global/state-dir.d.ts.map +1 -0
  49. package/dist/global/state-dir.js +43 -0
  50. package/dist/global/state-dir.js.map +1 -0
  51. package/dist/global/store.d.ts +2 -0
  52. package/dist/global/store.d.ts.map +1 -1
  53. package/dist/global/store.js.map +1 -1
  54. package/dist/loop/advice.d.ts +11 -0
  55. package/dist/loop/advice.d.ts.map +1 -0
  56. package/dist/loop/advice.js +48 -0
  57. package/dist/loop/advice.js.map +1 -0
  58. package/dist/loop/control.d.ts +15 -1
  59. package/dist/loop/control.d.ts.map +1 -1
  60. package/dist/loop/control.js +8 -1
  61. package/dist/loop/control.js.map +1 -1
  62. package/dist/loop/index.d.ts +1 -0
  63. package/dist/loop/index.d.ts.map +1 -1
  64. package/dist/loop/index.js +1 -0
  65. package/dist/loop/index.js.map +1 -1
  66. package/dist/service/manager.d.ts +2 -0
  67. package/dist/service/manager.d.ts.map +1 -1
  68. package/dist/service/manager.js +2 -1
  69. package/dist/service/manager.js.map +1 -1
  70. package/dist/service/unit.d.ts +16 -7
  71. package/dist/service/unit.d.ts.map +1 -1
  72. package/dist/service/unit.js +11 -7
  73. package/dist/service/unit.js.map +1 -1
  74. package/dist/store/file-store.d.ts +5 -18
  75. package/dist/store/file-store.d.ts.map +1 -1
  76. package/dist/store/file-store.js +1 -1
  77. package/dist/store/file-store.js.map +1 -1
  78. package/dist/store/index.d.ts +2 -1
  79. package/dist/store/index.d.ts.map +1 -1
  80. package/dist/store/index.js.map +1 -1
  81. package/dist/store/port.d.ts +53 -0
  82. package/dist/store/port.d.ts.map +1 -0
  83. package/dist/store/port.js +2 -0
  84. package/dist/store/port.js.map +1 -0
  85. package/package.json +1 -1
  86. package/skill/ghostrail/SKILL.md +2 -2
  87. package/templates/{code-local → code}/README.md +2 -2
  88. package/templates/{code-local → code}/ghostrail.toml +2 -2
  89. package/templates/{content-loop → content}/README.md +2 -2
  90. package/templates/{content-loop → content}/ghostrail.toml +1 -1
  91. /package/templates/{code-local → code}/prompts/resolve-issue.md +0 -0
  92. /package/templates/{code-local → code}/prompts/respond.md +0 -0
  93. /package/templates/{code-local → code}/prompts/triage.md +0 -0
  94. /package/templates/{content-loop → content}/prompts/draft.md +0 -0
  95. /package/templates/{content-loop → content}/prompts/respond.md +0 -0
@@ -10,7 +10,24 @@
10
10
  export interface Ghostrail {
11
11
  /** Display name, from `[factory].name` or the directory. Never an identity. */
12
12
  name: string;
13
- /** Absolute path to the repo. This is the identity. */
13
+ /**
14
+ * The repo this ghostrail is for, canonical (`host/owner/name`), or
15
+ * `local:<path>` for a repo with no usable remote. **This is the identity.**
16
+ *
17
+ * Optional only for backward compatibility: a registry written before repo
18
+ * identities existed has none, and an entry without one falls back to being
19
+ * keyed on its `path`, exactly as it was. This file is advertised as
20
+ * hand-editable, and refusing to read yesterday's one would be the worst
21
+ * possible reading of that promise.
22
+ */
23
+ repo?: string;
24
+ /**
25
+ * Absolute path to the checkout most recently used for this repo.
26
+ *
27
+ * A hint, for a human reading the file and for a local-worktree run that
28
+ * needs somewhere to make worktrees from. Not the identity: two checkouts of
29
+ * one repo are one ghostrail, which is the whole point of `repo`.
30
+ */
14
31
  path: string;
15
32
  /** ISO date the entry was added, for the operator. Nothing reads it. */
16
33
  added: string;
@@ -36,9 +53,10 @@ export declare const REGISTRY_FILE = "ghostrails.toml";
36
53
  * than fatal, so a registry written by a later ghostrail still reads here.
37
54
  * Order is preserved exactly, and the array is fresh on every call.
38
55
  *
39
- * Two entries at the same path are malformed for the same reason: a parsed
40
- * registry never contains duplicate paths, which is what lets `upsert` and
41
- * `findByPath` stay as simple as they are.
56
+ * Two entries for the same ghostrail are malformed for the same reason: a
57
+ * parsed registry never contains duplicate keys, which is what lets `upsert`
58
+ * and `findByKey` stay as simple as they are. The key is the repo identity, or
59
+ * the path for an entry written before identities existed.
42
60
  */
43
61
  export declare function parseRegistry(toml: string): Ghostrail[] | undefined;
44
62
  /**
@@ -51,10 +69,25 @@ export declare function parseRegistry(toml: string): Ghostrail[] | undefined;
51
69
  * the header invites them to do.
52
70
  */
53
71
  export declare function renderRegistry(entries: readonly Ghostrail[]): string;
72
+ /**
73
+ * What an entry is keyed on: its repo identity, or its path when it has none.
74
+ *
75
+ * One function so the parser, `findByKey`, and `upsert` cannot disagree about
76
+ * what "the same ghostrail" means, which is exactly the kind of drift that let
77
+ * two checkouts become two ghostrails in the first place.
78
+ */
79
+ export declare function entryKey(entry: Ghostrail): string;
54
80
  /** The entry at `path`, or undefined. Exact string equality; callers resolve first. */
55
81
  export declare function findByPath(entries: readonly Ghostrail[], path: string): Ghostrail | undefined;
82
+ /** The entry for a repo identity (or a path, for an entry that has none). */
83
+ export declare function findByKey(entries: readonly Ghostrail[], key: string): Ghostrail | undefined;
56
84
  /**
57
- * Add `entry`, or replace the one at the same path.
85
+ * Add `entry`, or replace the one with the same identity.
86
+ *
87
+ * Keyed on the repo, so `init` in a second checkout of a known repo updates
88
+ * that ghostrail (adopting the new checkout as its `path`) instead of adding a
89
+ * second one. An entry with no identity still matches on path, so a
90
+ * pre-identity registry keeps working.
58
91
  *
59
92
  * A replacement keeps its position so a diff of the file shows a changed block
60
93
  * rather than a move; a new entry appends. Neither argument is mutated.
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,sCAAsC;AACtC,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,uCAAuC;AACvC,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAyB/C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAS,CA2BnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAapE;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7F;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CAMnF"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,sCAAsC;AACtC,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,uCAAuC;AACvC,eAAO,MAAM,aAAa,oBAAoB,CAAC;AA2B/C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAS,CA4BnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAcpE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAEjD;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7F;AAED,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE3F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CAOnF"}
@@ -17,12 +17,15 @@ function isRecord(value) {
17
17
  function parseEntry(raw) {
18
18
  if (!isRecord(raw))
19
19
  return undefined;
20
- const { name, path, added, watch } = raw;
20
+ const { name, repo, path, added, watch } = raw;
21
21
  if (typeof name !== "string" || typeof path !== "string")
22
22
  return undefined;
23
23
  if (typeof added !== "string" || typeof watch !== "boolean")
24
24
  return undefined;
25
- return { name, path, added, watch };
25
+ // Absent is legal (a pre-identity registry); present but not a string is not.
26
+ if (repo !== undefined && typeof repo !== "string")
27
+ return undefined;
28
+ return { name, ...(repo === undefined ? {} : { repo }), path, added, watch };
26
29
  }
27
30
  /**
28
31
  * Parse the registry, or undefined when it cannot be trusted.
@@ -38,9 +41,10 @@ function parseEntry(raw) {
38
41
  * than fatal, so a registry written by a later ghostrail still reads here.
39
42
  * Order is preserved exactly, and the array is fresh on every call.
40
43
  *
41
- * Two entries at the same path are malformed for the same reason: a parsed
42
- * registry never contains duplicate paths, which is what lets `upsert` and
43
- * `findByPath` stay as simple as they are.
44
+ * Two entries for the same ghostrail are malformed for the same reason: a
45
+ * parsed registry never contains duplicate keys, which is what lets `upsert`
46
+ * and `findByKey` stay as simple as they are. The key is the repo identity, or
47
+ * the path for an entry written before identities existed.
44
48
  */
45
49
  export function parseRegistry(toml) {
46
50
  let root;
@@ -58,18 +62,19 @@ export function parseRegistry(toml) {
58
62
  if (!Array.isArray(raw))
59
63
  return undefined;
60
64
  const entries = [];
61
- const paths = new Set();
65
+ const keys = new Set();
62
66
  for (const item of raw) {
63
67
  const entry = parseEntry(item);
64
68
  if (entry === undefined)
65
69
  return undefined;
66
- // `path` is the identity, so two blocks claiming it is a contradiction
67
- // rather than a preference to resolve. Copying a block and forgetting to
68
- // change the path is a plausible hand-edit, and honouring either one of
69
- // them would silently ignore an edit the operator clearly meant.
70
- if (paths.has(entry.path))
70
+ // Two blocks claiming one ghostrail is a contradiction rather than a
71
+ // preference to resolve. Copying a block and forgetting to change the repo
72
+ // is a plausible hand-edit, and honouring either one of them would silently
73
+ // ignore an edit the operator clearly meant.
74
+ const key = entryKey(entry);
75
+ if (keys.has(key))
71
76
  return undefined;
72
- paths.add(entry.path);
77
+ keys.add(key);
73
78
  entries.push(entry);
74
79
  }
75
80
  return entries;
@@ -91,6 +96,7 @@ export function renderRegistry(entries) {
91
96
  const blocks = entries.map((entry) => [
92
97
  "[[ghostrail]]",
93
98
  `name = ${quote(entry.name)}`,
99
+ ...(entry.repo === undefined ? [] : [`repo = ${quote(entry.repo)}`]),
94
100
  `path = ${quote(entry.path)}`,
95
101
  `added = ${quote(entry.added)}`,
96
102
  `watch = ${entry.watch}`,
@@ -99,18 +105,38 @@ export function renderRegistry(entries) {
99
105
  // run together the blocks are hard to read and give noisier diffs.
100
106
  return `${[HEADER, ...blocks].join("\n\n")}\n`;
101
107
  }
108
+ /**
109
+ * What an entry is keyed on: its repo identity, or its path when it has none.
110
+ *
111
+ * One function so the parser, `findByKey`, and `upsert` cannot disagree about
112
+ * what "the same ghostrail" means, which is exactly the kind of drift that let
113
+ * two checkouts become two ghostrails in the first place.
114
+ */
115
+ export function entryKey(entry) {
116
+ return entry.repo ?? entry.path;
117
+ }
102
118
  /** The entry at `path`, or undefined. Exact string equality; callers resolve first. */
103
119
  export function findByPath(entries, path) {
104
120
  return entries.find((entry) => entry.path === path);
105
121
  }
122
+ /** The entry for a repo identity (or a path, for an entry that has none). */
123
+ export function findByKey(entries, key) {
124
+ return entries.find((entry) => entryKey(entry) === key);
125
+ }
106
126
  /**
107
- * Add `entry`, or replace the one at the same path.
127
+ * Add `entry`, or replace the one with the same identity.
128
+ *
129
+ * Keyed on the repo, so `init` in a second checkout of a known repo updates
130
+ * that ghostrail (adopting the new checkout as its `path`) instead of adding a
131
+ * second one. An entry with no identity still matches on path, so a
132
+ * pre-identity registry keeps working.
108
133
  *
109
134
  * A replacement keeps its position so a diff of the file shows a changed block
110
135
  * rather than a move; a new entry appends. Neither argument is mutated.
111
136
  */
112
137
  export function upsert(entries, entry) {
113
- const index = entries.findIndex((existing) => existing.path === entry.path);
138
+ const key = entryKey(entry);
139
+ const index = entries.findIndex((existing) => entryKey(existing) === key);
114
140
  if (index === -1)
115
141
  return [...entries, entry];
116
142
  const next = [...entries];
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The canonical identity of the repo a git remote points at, as
3
+ * `host[:port]/path/to/repo`, or undefined when the remote is not one.
4
+ *
5
+ * The point is that every spelling of one repo collapses to one string:
6
+ * scp-like and URL forms, with or without `.git`, with or without a default
7
+ * port, are the same repo and must key the same ghostrail.
8
+ *
9
+ * Strict everywhere except case. Anything unrecognised is undefined rather than
10
+ * guessed, because the two failure directions are not symmetric: wrongly
11
+ * merging two repos pools their history and cannot be undone, while wrongly
12
+ * separating two clones only leaves today's behavior in place. The exception is
13
+ * lowercasing, which is deliberately lenient: hosts are case-insensitive and so
14
+ * are repos on the forges people use, so `Owner/Repo` and `owner/repo` are one
15
+ * repo. On a case-sensitive host that could merge two repos differing only in
16
+ * case, which is judged far less likely than someone typing a clone URL with
17
+ * different capitalisation.
18
+ *
19
+ * Never throws. A remote that cannot be read is a missing identity, and the
20
+ * caller falls back to {@link localRepoId}.
21
+ */
22
+ export declare function canonicalRepoId(remote: string): string | undefined;
23
+ /**
24
+ * The identity of a repo with no usable remote, from its absolute path.
25
+ *
26
+ * The `local:` prefix means it can never collide with a `host/...` identity,
27
+ * and makes a path-keyed entry visibly path-keyed rather than silently so. The
28
+ * path is kept verbatim: it is already absolute, and case-folding it would be
29
+ * wrong on a case-sensitive filesystem.
30
+ */
31
+ export declare function localRepoId(path: string): string;
32
+ /**
33
+ * Whether an identity is path-keyed rather than derived from a remote.
34
+ *
35
+ * Checks `local:/`, not `local:`. The shorter prefix looks like the marker and
36
+ * is not one: a host genuinely named `local` on a non-default port produces
37
+ * `local:22/owner/repo`, which is an ordinary canonical identity.
38
+ */
39
+ export declare function isLocalRepoId(id: string): boolean;
40
+ /**
41
+ * A filesystem-safe directory name for an identity.
42
+ *
43
+ * The hash is not decoration. Folding characters and truncating both lose
44
+ * information, so two different identities can reduce to the same readable
45
+ * stem; the hash of the *full* identity is what keeps the key unique. The stem
46
+ * is there so a human browsing the state directory can tell what they are
47
+ * looking at.
48
+ */
49
+ export declare function repoKey(id: string): string;
50
+ //# sourceMappingURL=repo-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-id.d.ts","sourceRoot":"","sources":["../../src/global/repo-id.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAgBlE;AAqDD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAU1C"}
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * What identifies a ghostrail: the repo it works on, not the directory it was
4
+ * set up from. Pure, so the normalization rules that decide whether two clones
5
+ * are one repo can be checked without a git repository.
6
+ */
7
+ /** Ports that carry no information, per scheme. */
8
+ const DEFAULT_PORTS = {
9
+ ssh: "22",
10
+ git: "9418",
11
+ http: "80",
12
+ https: "443",
13
+ };
14
+ /** Longest readable stem in a {@link repoKey}, before the hash is appended. */
15
+ const MAX_STEM = 48;
16
+ /**
17
+ * The canonical identity of the repo a git remote points at, as
18
+ * `host[:port]/path/to/repo`, or undefined when the remote is not one.
19
+ *
20
+ * The point is that every spelling of one repo collapses to one string:
21
+ * scp-like and URL forms, with or without `.git`, with or without a default
22
+ * port, are the same repo and must key the same ghostrail.
23
+ *
24
+ * Strict everywhere except case. Anything unrecognised is undefined rather than
25
+ * guessed, because the two failure directions are not symmetric: wrongly
26
+ * merging two repos pools their history and cannot be undone, while wrongly
27
+ * separating two clones only leaves today's behavior in place. The exception is
28
+ * lowercasing, which is deliberately lenient: hosts are case-insensitive and so
29
+ * are repos on the forges people use, so `Owner/Repo` and `owner/repo` are one
30
+ * repo. On a case-sensitive host that could merge two repos differing only in
31
+ * case, which is judged far less likely than someone typing a clone URL with
32
+ * different capitalisation.
33
+ *
34
+ * Never throws. A remote that cannot be read is a missing identity, and the
35
+ * caller falls back to {@link localRepoId}.
36
+ */
37
+ export function canonicalRepoId(remote) {
38
+ const trimmed = remote.trim();
39
+ if (trimmed.length === 0)
40
+ return undefined;
41
+ const parsed = splitRemote(trimmed);
42
+ if (parsed === undefined)
43
+ return undefined;
44
+ const path = parsed.path
45
+ .split("/")
46
+ .filter((segment) => segment.length > 0)
47
+ .join("/")
48
+ .replace(/\.git$/i, "");
49
+ // A bare host names no repo, and neither does a path that was only slashes.
50
+ if (path.length === 0)
51
+ return undefined;
52
+ return `${parsed.host}/${path}`.toLowerCase();
53
+ }
54
+ /** Host (with any non-default port) and raw path, or undefined. */
55
+ function splitRemote(remote) {
56
+ // A local path is not a remote. Checked first so `/srv/git/repo` and
57
+ // `C:\repos\thing` never reach the URL branch and acquire a bogus host.
58
+ if (remote.startsWith("/") || remote.startsWith(".") || /^[a-z]:[\\/]/i.test(remote)) {
59
+ return undefined;
60
+ }
61
+ const url = remote.match(/^([a-z][a-z0-9+.-]*):\/\/([^/\s]+)(\/.*)?$/i);
62
+ if (url !== null) {
63
+ const [, scheme = "", authority = "", path = ""] = url;
64
+ // `file://` addresses a directory, not a remote repo identity.
65
+ if (scheme.toLowerCase() === "file")
66
+ return undefined;
67
+ return { host: stripPort(dropUserinfo(authority), scheme.toLowerCase()), path };
68
+ }
69
+ // scp-like: [user@]host:path. The colon must not be followed by `/`, which
70
+ // would make it a scheme, and the host must carry no slash.
71
+ const scp = remote.match(/^([^/\s]+):(?!\/)(.+)$/);
72
+ if (scp !== null) {
73
+ const [, authority = "", path = ""] = scp;
74
+ return { host: stripPort(dropUserinfo(authority), "ssh"), path };
75
+ }
76
+ return undefined;
77
+ }
78
+ /** Drop `user@` and any password: a credential is not part of an identity. */
79
+ function dropUserinfo(authority) {
80
+ const at = authority.lastIndexOf("@");
81
+ return at === -1 ? authority : authority.slice(at + 1);
82
+ }
83
+ /**
84
+ * Drop the scheme's default port, and any empty one. Keep every other.
85
+ *
86
+ * The empty case is not a curiosity. `ssh://local:/owner/repo` left the host as
87
+ * `local:` and produced `local:/owner/repo`, which is character-for-character a
88
+ * {@link localRepoId} for `/owner/repo`: a remote repo and a path-keyed one
89
+ * colliding on a single identity. A trailing colon with no digits is not a
90
+ * port, and treating it as part of the host was the whole bug.
91
+ */
92
+ function stripPort(host, scheme) {
93
+ const empty = host.match(/^(.*):$/);
94
+ if (empty !== null)
95
+ return empty[1] ?? host;
96
+ const match = host.match(/^(.*):(\d+)$/);
97
+ if (match === null)
98
+ return host;
99
+ const [, name = "", port = ""] = match;
100
+ return port === DEFAULT_PORTS[scheme] ? name : host;
101
+ }
102
+ /**
103
+ * The identity of a repo with no usable remote, from its absolute path.
104
+ *
105
+ * The `local:` prefix means it can never collide with a `host/...` identity,
106
+ * and makes a path-keyed entry visibly path-keyed rather than silently so. The
107
+ * path is kept verbatim: it is already absolute, and case-folding it would be
108
+ * wrong on a case-sensitive filesystem.
109
+ */
110
+ export function localRepoId(path) {
111
+ // Always `local:/...`, never bare `local:`. A canonical identity puts a port
112
+ // after the colon and a path segment after that, so `local:22/a/a` is a real
113
+ // canonical id for a host named `local`. The two can never be equal, but only
114
+ // because a local id's colon is always followed by `/` and a canonical one's
115
+ // never is. Guaranteeing that here, rather than relying on every caller to
116
+ // pass an absolute path, is what makes {@link isLocalRepoId} safe.
117
+ return path.startsWith("/") ? `local:${path}` : `local:/${path}`;
118
+ }
119
+ /**
120
+ * Whether an identity is path-keyed rather than derived from a remote.
121
+ *
122
+ * Checks `local:/`, not `local:`. The shorter prefix looks like the marker and
123
+ * is not one: a host genuinely named `local` on a non-default port produces
124
+ * `local:22/owner/repo`, which is an ordinary canonical identity.
125
+ */
126
+ export function isLocalRepoId(id) {
127
+ return id.startsWith("local:/");
128
+ }
129
+ /**
130
+ * A filesystem-safe directory name for an identity.
131
+ *
132
+ * The hash is not decoration. Folding characters and truncating both lose
133
+ * information, so two different identities can reduce to the same readable
134
+ * stem; the hash of the *full* identity is what keeps the key unique. The stem
135
+ * is there so a human browsing the state directory can tell what they are
136
+ * looking at.
137
+ */
138
+ export function repoKey(id) {
139
+ const hash = createHash("sha256").update(id, "utf8").digest("hex").slice(0, 8);
140
+ const stem = id
141
+ .toLowerCase()
142
+ .replace(/[^a-z0-9._-]+/g, "-")
143
+ .replace(/-+/g, "-")
144
+ .replace(/^-+|-+$/g, "")
145
+ .slice(0, MAX_STEM)
146
+ .replace(/-+$/, "");
147
+ return stem.length === 0 ? hash : `${stem}-${hash}`;
148
+ }
149
+ //# sourceMappingURL=repo-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-id.js","sourceRoot":"","sources":["../../src/global/repo-id.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { GlobalEnv } from "./store.js";
2
+ /**
3
+ * Where a ghostrail's state lives: its runs, events, artifacts, lock, pause
4
+ * flag, and respond watermarks.
5
+ *
6
+ * Outside the repo, keyed by the repo's identity. Inside the checkout, all of
7
+ * it was per-directory, so two clones of one repo had two stores and two locks
8
+ * and therefore raced each other, split their history, and each got a full
9
+ * `max_cost_usd_per_run` ceiling of their own.
10
+ */
11
+ /**
12
+ * The root for ghostrail's state, honouring `$XDG_STATE_HOME`.
13
+ *
14
+ * State, not config, so this is the state dir rather than the config dir the
15
+ * registry and `secrets.env` live in. The XDG spec is explicit that state is
16
+ * for data that persists between runs but is neither configuration nor a cache,
17
+ * which is exactly a run history.
18
+ *
19
+ * `GHOSTRAIL_HOME` still wins, as it does for the global dir: a caller who has
20
+ * pinned everything to one directory means it, and splitting their state
21
+ * somewhere else would be a surprise. Tests rely on this too.
22
+ */
23
+ export declare function stateHome(env: GlobalEnv, homedir: string): string;
24
+ /**
25
+ * The state directory for one repo: `<stateHome>/repos/<key>`.
26
+ *
27
+ * `repos/` rather than the root so the layout has room for state that belongs
28
+ * to the account rather than to a repo, without a future addition colliding
29
+ * with a repo whose key happens to match.
30
+ */
31
+ export declare function repoStateDir(repoId: string, env: GlobalEnv, homedir: string): string;
32
+ //# sourceMappingURL=state-dir.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-dir.d.ts","sourceRoot":"","sources":["../../src/global/state-dir.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CASjE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEpF"}
@@ -0,0 +1,43 @@
1
+ import { join } from "node:path";
2
+ import { repoKey } from "./repo-id.js";
3
+ /**
4
+ * Where a ghostrail's state lives: its runs, events, artifacts, lock, pause
5
+ * flag, and respond watermarks.
6
+ *
7
+ * Outside the repo, keyed by the repo's identity. Inside the checkout, all of
8
+ * it was per-directory, so two clones of one repo had two stores and two locks
9
+ * and therefore raced each other, split their history, and each got a full
10
+ * `max_cost_usd_per_run` ceiling of their own.
11
+ */
12
+ /**
13
+ * The root for ghostrail's state, honouring `$XDG_STATE_HOME`.
14
+ *
15
+ * State, not config, so this is the state dir rather than the config dir the
16
+ * registry and `secrets.env` live in. The XDG spec is explicit that state is
17
+ * for data that persists between runs but is neither configuration nor a cache,
18
+ * which is exactly a run history.
19
+ *
20
+ * `GHOSTRAIL_HOME` still wins, as it does for the global dir: a caller who has
21
+ * pinned everything to one directory means it, and splitting their state
22
+ * somewhere else would be a surprise. Tests rely on this too.
23
+ */
24
+ export function stateHome(env, homedir) {
25
+ if (env.GHOSTRAIL_HOME !== undefined && env.GHOSTRAIL_HOME.length > 0) {
26
+ return join(env.GHOSTRAIL_HOME, "state");
27
+ }
28
+ const base = env.XDG_STATE_HOME !== undefined && env.XDG_STATE_HOME.length > 0
29
+ ? env.XDG_STATE_HOME
30
+ : join(homedir, ".local", "state");
31
+ return join(base, "ghostrail");
32
+ }
33
+ /**
34
+ * The state directory for one repo: `<stateHome>/repos/<key>`.
35
+ *
36
+ * `repos/` rather than the root so the layout has room for state that belongs
37
+ * to the account rather than to a repo, without a future addition colliding
38
+ * with a repo whose key happens to match.
39
+ */
40
+ export function repoStateDir(repoId, env, homedir) {
41
+ return join(stateHome(env, homedir), "repos", repoKey(repoId));
42
+ }
43
+ //# sourceMappingURL=state-dir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-dir.js","sourceRoot":"","sources":["../../src/global/state-dir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,SAAS,CAAC,GAAc,EAAE,OAAe;IACvD,IAAI,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,GAAG,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtE,OAAO,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IACD,MAAM,IAAI,GACR,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,GAAG,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;QAC/D,CAAC,CAAC,GAAG,CAAC,cAAc;QACpB,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,GAAc,EAAE,OAAe;IAC1E,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AACjE,CAAC"}
@@ -2,6 +2,8 @@ import { type Ghostrail } from "./registry.js";
2
2
  export interface GlobalEnv {
3
3
  GHOSTRAIL_HOME?: string;
4
4
  XDG_CONFIG_HOME?: string;
5
+ /** Where run state goes. Config and state are different XDG directories. */
6
+ XDG_STATE_HOME?: string;
5
7
  }
6
8
  /**
7
9
  * Resolve the global ghostrail config directory that holds account-level
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAgD,MAAM,eAAe,CAAC;AAE7F,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxE;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,mEAAmE;IAC7D,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjD,wDAAwD;IAClD,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQxC,YAAY,IAAI,MAAM;IAItB;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC;IAUtD,8EAA8E;IACxE,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,4EAA4E;IACtE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAU9D"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAgD,MAAM,eAAe,CAAC;AAE7F,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxE;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,mEAAmE;IAC7D,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjD,wDAAwD;IAClD,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQxC,YAAY,IAAI,MAAM;IAItB;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC;IAUtD,8EAA8E;IACxE,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,4EAA4E;IACtE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAU9D"}
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { GhostrailConfig } from "../config/schema.js";
2
+ /**
3
+ * One line about what concurrent runs will share, or undefined when there is
4
+ * nothing worth saying.
5
+ *
6
+ * Silent at `concurrency` 1, because nothing overlaps and a warning on the
7
+ * default configuration is noise that teaches people to ignore warnings. Silent
8
+ * on a backend that clones per run, because the sharing does not exist there.
9
+ */
10
+ export declare function concurrencyAdvice(config: GhostrailConfig): string | undefined;
11
+ //# sourceMappingURL=advice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"advice.d.ts","sourceRoot":"","sources":["../../src/loop/advice.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAsBxE;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,SAAS,CAmB7E"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What raising `concurrency` costs, on the backend you actually have.
3
+ *
4
+ * Not a gate. Six concurrent `git worktree add -B` calls against one repo were
5
+ * tried before any of this was written, and all six succeeded: git is not the
6
+ * constraint, so refusing would be the tool overruling someone who knows their
7
+ * own setup. What is worth saying is what concurrent runs share, which depends
8
+ * on the backend and on whether the factory owns its clone.
9
+ */
10
+ /**
11
+ * Backends that give each run its own filesystem by cloning into it.
12
+ *
13
+ * Listed by what they do rather than by excluding `local-worktree`, so a
14
+ * backend added later does not silently inherit a warning that does not apply
15
+ * to it. Getting added here is a claim about isolation, which is the right
16
+ * thing to have to make deliberately.
17
+ */
18
+ const CLONES_PER_RUN = ["container", "firecracker", "e2b", "remote"];
19
+ /**
20
+ * One line about what concurrent runs will share, or undefined when there is
21
+ * nothing worth saying.
22
+ *
23
+ * Silent at `concurrency` 1, because nothing overlaps and a warning on the
24
+ * default configuration is noise that teaches people to ignore warnings. Silent
25
+ * on a backend that clones per run, because the sharing does not exist there.
26
+ */
27
+ export function concurrencyAdvice(config) {
28
+ const { concurrency } = config.guardrails;
29
+ if (!Number.isFinite(concurrency) || concurrency <= 1)
30
+ return undefined;
31
+ if (CLONES_PER_RUN.includes(config.backend.kind))
32
+ return undefined;
33
+ // Only when the factory is still borrowing the operator's checkout. With its
34
+ // own clone (TJ-1586) this hazard is gone, and a warning describing something
35
+ // the operator already turned off costs trust in every other warning.
36
+ const shared = [
37
+ ...(config.backend.managedCheckout ? [] : ["a git object store shared with your own work"]),
38
+ "your tool caches (one pnpm/cargo store for every gate at once)",
39
+ "this machine (N agents plus N gate runs)",
40
+ ];
41
+ // Joined, not newline-separated: the caller decides framing, and a warning
42
+ // that smuggles line breaks into someone else's log format is rude.
43
+ return [
44
+ `concurrency ${concurrency} on local-worktree: runs will share ${shared.join(", ")}.`,
45
+ "The container backend gives each run its own filesystem.",
46
+ ].join(" ");
47
+ }
48
+ //# sourceMappingURL=advice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"advice.js","sourceRoot":"","sources":["../../src/loop/advice.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,MAAM,cAAc,GAA2B,CAAC,WAAW,EAAE,aAAa,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;AAE7F;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAuB;IACvD,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACxE,IAAI,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAEnE,6EAA6E;IAC7E,8EAA8E;IAC9E,sEAAsE;IACtE,MAAM,MAAM,GAAG;QACb,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,8CAA8C,CAAC,CAAC;QAC3F,gEAAgE;QAChE,0CAA0C;KAC3C,CAAC;IACF,2EAA2E;IAC3E,oEAAoE;IACpE,OAAO;QACL,eAAe,WAAW,uCAAuC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;QACrF,0DAA0D;KAC3D,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC"}
@@ -2,7 +2,7 @@
2
2
  export interface ControlState {
3
3
  enabled: boolean;
4
4
  }
5
- /** The control file's path inside a store directory (e.g. `<repo>/.factory`). */
5
+ /** The control file's path inside a store directory (this repo's state dir). */
6
6
  export declare function controlPath(dir: string): string;
7
7
  /**
8
8
  * Read the run/pause state written for a ghostrail. Absent, unreadable, or
@@ -12,4 +12,18 @@ export declare function controlPath(dir: string): string;
12
12
  export declare function readControl(dir: string): Promise<ControlState>;
13
13
  /** Persist the run/pause state so any process reading the same directory sees it. */
14
14
  export declare function writeControl(dir: string, state: ControlState): Promise<void>;
15
+ /**
16
+ * Reading and writing a ghostrail's run/pause flag, behind a port.
17
+ *
18
+ * Separate from the run store because they are different concerns that only
19
+ * happened to share a directory. The board used to reach through the store to
20
+ * its `dir` to get at this, which worked exactly as long as every store was a
21
+ * directory on this machine.
22
+ */
23
+ export interface RunControl {
24
+ read(): Promise<ControlState>;
25
+ write(state: ControlState): Promise<void>;
26
+ }
27
+ /** The control flag as a file in `dir`, which is what a local ghostrail has. */
28
+ export declare function fileControl(dir: string): RunControl;
15
29
  //# sourceMappingURL=control.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/loop/control.ts"],"names":[],"mappings":"AAGA,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAWpE;AAED,qFAAqF;AACrF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF"}
1
+ {"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../src/loop/control.ts"],"names":[],"mappings":"AAGA,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB;AAID,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAWpE;AAED,qFAAqF;AACrF,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAGlF;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IAC9B,KAAK,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAKnD"}