@manyoars/oars 0.1.1 → 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.
- package/README.md +134 -26
- package/dist/api.js +1 -1
- package/dist/bin.js +6 -29
- package/dist/cli.d.ts +6 -3
- package/dist/cli.js +370 -72
- package/dist/commands/keys.d.ts +31 -0
- package/dist/commands/keys.js +65 -0
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +3 -5
- package/dist/errors.d.ts +13 -13
- package/dist/errors.js +28 -11
- package/dist/gate/checks.d.ts +68 -0
- package/dist/gate/checks.js +584 -0
- package/dist/gate/command.d.ts +14 -0
- package/dist/gate/command.js +61 -0
- package/dist/gate/hook-fill-main.d.ts +1 -0
- package/dist/gate/hook-fill-main.js +16 -0
- package/dist/gate/hook-fill.d.ts +38 -0
- package/dist/gate/hook-fill.js +94 -0
- package/dist/gate/hook-main.d.ts +1 -0
- package/dist/gate/hook-main.js +35 -0
- package/dist/gate/hook-process.d.ts +14 -0
- package/dist/gate/hook-process.js +64 -0
- package/dist/gate/hook.d.ts +46 -0
- package/dist/gate/hook.js +256 -0
- package/dist/gate/install.d.ts +25 -0
- package/dist/gate/install.js +126 -0
- package/dist/gate/local-bin.d.ts +8 -0
- package/dist/gate/local-bin.js +36 -0
- package/dist/gate/notes.d.ts +19 -0
- package/dist/gate/notes.js +27 -0
- package/dist/gate/novelty.d.ts +35 -0
- package/dist/gate/novelty.js +99 -0
- package/dist/gate/policy.d.ts +25 -0
- package/dist/gate/policy.js +73 -0
- package/dist/gate/record.d.ts +120 -0
- package/dist/gate/record.js +125 -0
- package/dist/gate/registry-state.d.ts +10 -0
- package/dist/gate/registry-state.js +30 -0
- package/dist/gate/registry.d.ts +56 -0
- package/dist/gate/registry.js +94 -0
- package/dist/gate/replay.d.ts +20 -0
- package/dist/gate/replay.js +257 -0
- package/dist/gate/report.d.ts +50 -0
- package/dist/gate/report.js +89 -0
- package/dist/gate/scope.d.ts +7 -0
- package/dist/gate/scope.js +27 -0
- package/dist/gate/transcripts.d.ts +68 -0
- package/dist/gate/transcripts.js +232 -0
- package/dist/hook-bin.d.ts +1 -0
- package/dist/hook-bin.js +25 -0
- package/dist/issuer.d.ts +16 -0
- package/dist/issuer.js +55 -0
- package/dist/main.d.ts +1 -0
- package/dist/main.js +64 -0
- package/dist/oidc.js +1 -1
- package/dist/sessions/actions.d.ts +35 -0
- package/dist/sessions/actions.js +351 -0
- package/dist/sessions/allow.d.ts +14 -0
- package/dist/sessions/allow.js +51 -0
- package/dist/sessions/archive.d.ts +11 -0
- package/dist/sessions/archive.js +38 -0
- package/dist/sessions/audit.d.ts +42 -0
- package/dist/sessions/audit.js +104 -0
- package/dist/sessions/baseline.d.ts +62 -0
- package/dist/sessions/baseline.js +188 -0
- package/dist/sessions/capture-runtime.d.ts +10 -0
- package/dist/sessions/capture-runtime.js +12 -0
- package/dist/sessions/cell-increments.d.ts +43 -0
- package/dist/sessions/cell-increments.js +209 -0
- package/dist/sessions/cells.d.ts +79 -0
- package/dist/sessions/cells.js +181 -0
- package/dist/sessions/codex.d.ts +53 -0
- package/dist/sessions/codex.js +679 -0
- package/dist/sessions/command.d.ts +117 -0
- package/dist/sessions/command.js +373 -0
- package/dist/sessions/copilot.d.ts +40 -0
- package/dist/sessions/copilot.js +432 -0
- package/dist/sessions/cursor.d.ts +24 -0
- package/dist/sessions/cursor.js +324 -0
- package/dist/sessions/derive.d.ts +57 -0
- package/dist/sessions/derive.js +320 -0
- package/dist/sessions/exclusion.d.ts +81 -0
- package/dist/sessions/exclusion.js +353 -0
- package/dist/sessions/extract.d.ts +179 -0
- package/dist/sessions/extract.js +1774 -0
- package/dist/sessions/fold.d.ts +17 -0
- package/dist/sessions/fold.js +71 -0
- package/dist/sessions/frame.d.ts +24 -0
- package/dist/sessions/frame.js +95 -0
- package/dist/sessions/gemini.d.ts +37 -0
- package/dist/sessions/gemini.js +613 -0
- package/dist/sessions/gold-arms.d.ts +67 -0
- package/dist/sessions/gold-arms.js +172 -0
- package/dist/sessions/gold.d.ts +197 -0
- package/dist/sessions/gold.js +463 -0
- package/dist/sessions/graph.d.ts +12 -0
- package/dist/sessions/graph.js +34 -0
- package/dist/sessions/grok.d.ts +36 -0
- package/dist/sessions/grok.js +421 -0
- package/dist/sessions/hook-calls.d.ts +3 -0
- package/dist/sessions/hook-calls.js +47 -0
- package/dist/sessions/hook-index.d.ts +53 -0
- package/dist/sessions/hook-index.js +157 -0
- package/dist/sessions/hooks.d.ts +13 -0
- package/dist/sessions/hooks.js +170 -0
- package/dist/sessions/identity.d.ts +22 -0
- package/dist/sessions/identity.js +101 -0
- package/dist/sessions/install.d.ts +54 -0
- package/dist/sessions/install.js +201 -0
- package/dist/sessions/lineage.d.ts +19 -0
- package/dist/sessions/lineage.js +118 -0
- package/dist/sessions/mcp.d.ts +6 -0
- package/dist/sessions/mcp.js +153 -0
- package/dist/sessions/merkle.d.ts +7 -0
- package/dist/sessions/merkle.js +28 -0
- package/dist/sessions/motif.d.ts +43 -0
- package/dist/sessions/motif.js +59 -0
- package/dist/sessions/package-resolve.d.ts +30 -0
- package/dist/sessions/package-resolve.js +183 -0
- package/dist/sessions/pending.d.ts +34 -0
- package/dist/sessions/pending.js +142 -0
- package/dist/sessions/pool-command.d.ts +44 -0
- package/dist/sessions/pool-command.js +542 -0
- package/dist/sessions/pool-delta.d.ts +54 -0
- package/dist/sessions/pool-delta.js +190 -0
- package/dist/sessions/pool-outbox.d.ts +63 -0
- package/dist/sessions/pool-outbox.js +260 -0
- package/dist/sessions/pool-print.d.ts +26 -0
- package/dist/sessions/pool-print.js +68 -0
- package/dist/sessions/pool-wire.d.ts +225 -0
- package/dist/sessions/pool-wire.js +620 -0
- package/dist/sessions/pool.d.ts +32 -0
- package/dist/sessions/pool.js +164 -0
- package/dist/sessions/projection.d.ts +75 -0
- package/dist/sessions/projection.js +165 -0
- package/dist/sessions/public-vocabulary.d.ts +73 -0
- package/dist/sessions/public-vocabulary.js +302 -0
- package/dist/sessions/query.d.ts +111 -0
- package/dist/sessions/query.js +246 -0
- package/dist/sessions/read-adoption.d.ts +2 -0
- package/dist/sessions/read-adoption.js +27 -0
- package/dist/sessions/read-call-shape.d.ts +2 -0
- package/dist/sessions/read-call-shape.js +25 -0
- package/dist/sessions/read-change.d.ts +2 -0
- package/dist/sessions/read-change.js +30 -0
- package/dist/sessions/read-cost.d.ts +4 -0
- package/dist/sessions/read-cost.js +37 -0
- package/dist/sessions/read-day-move.d.ts +3 -0
- package/dist/sessions/read-day-move.js +65 -0
- package/dist/sessions/read-fit.d.ts +7 -0
- package/dist/sessions/read-fit.js +41 -0
- package/dist/sessions/read-interaction.d.ts +5 -0
- package/dist/sessions/read-interaction.js +62 -0
- package/dist/sessions/read-lead-lag.d.ts +15 -0
- package/dist/sessions/read-lead-lag.js +81 -0
- package/dist/sessions/read-live.d.ts +6 -0
- package/dist/sessions/read-live.js +38 -0
- package/dist/sessions/read-method.d.ts +2 -0
- package/dist/sessions/read-method.js +29 -0
- package/dist/sessions/read-next-step.d.ts +2 -0
- package/dist/sessions/read-next-step.js +35 -0
- package/dist/sessions/read-order.d.ts +54 -0
- package/dist/sessions/read-order.js +62 -0
- package/dist/sessions/read-readable.d.ts +8 -0
- package/dist/sessions/read-readable.js +24 -0
- package/dist/sessions/read-recovery.d.ts +20 -0
- package/dist/sessions/read-recovery.js +97 -0
- package/dist/sessions/read-rows.d.ts +13 -0
- package/dist/sessions/read-rows.js +47 -0
- package/dist/sessions/read-substitution.d.ts +5 -0
- package/dist/sessions/read-substitution.js +51 -0
- package/dist/sessions/read-support.d.ts +48 -0
- package/dist/sessions/read-support.js +73 -0
- package/dist/sessions/read-tool-day.d.ts +2 -0
- package/dist/sessions/read-tool-day.js +26 -0
- package/dist/sessions/read-version-boundary.d.ts +31 -0
- package/dist/sessions/read-version-boundary.js +95 -0
- package/dist/sessions/read.d.ts +79 -0
- package/dist/sessions/read.js +84 -0
- package/dist/sessions/recall-consequence.d.ts +45 -0
- package/dist/sessions/recall-consequence.js +179 -0
- package/dist/sessions/recall-object.d.ts +137 -0
- package/dist/sessions/recall-object.js +77 -0
- package/dist/sessions/recall-pool.d.ts +19 -0
- package/dist/sessions/recall-pool.js +96 -0
- package/dist/sessions/recall-push.d.ts +26 -0
- package/dist/sessions/recall-push.js +95 -0
- package/dist/sessions/recall-reads.d.ts +117 -0
- package/dist/sessions/recall-reads.js +410 -0
- package/dist/sessions/recall-render.d.ts +19 -0
- package/dist/sessions/recall-render.js +168 -0
- package/dist/sessions/redact.d.ts +6 -0
- package/dist/sessions/redact.js +54 -0
- package/dist/sessions/repo-root.d.ts +10 -0
- package/dist/sessions/repo-root.js +101 -0
- package/dist/sessions/schema.d.ts +644 -0
- package/dist/sessions/schema.js +148 -0
- package/dist/sessions/session-shape.d.ts +73 -0
- package/dist/sessions/session-shape.js +342 -0
- package/dist/sessions/sources.d.ts +50 -0
- package/dist/sessions/sources.js +159 -0
- package/dist/sessions/steps.d.ts +105 -0
- package/dist/sessions/steps.js +250 -0
- package/dist/sessions/store.d.ts +297 -0
- package/dist/sessions/store.js +1478 -0
- package/dist/sessions/strata.d.ts +26 -0
- package/dist/sessions/strata.js +77 -0
- package/dist/sessions/survival.d.ts +25 -0
- package/dist/sessions/survival.js +230 -0
- package/dist/sessions/verify.d.ts +53 -0
- package/dist/sessions/verify.js +286 -0
- package/dist/sessions/vocab-load.d.ts +26 -0
- package/dist/sessions/vocab-load.js +61 -0
- package/dist/sessions/vscode-copilot.d.ts +38 -0
- package/dist/sessions/vscode-copilot.js +477 -0
- package/package.json +4 -5
- package/vocab/crates.json +1 -0
- package/vocab/go.json +1 -0
- package/vocab/harness.json +1 -0
- package/vocab/homebrew.json +1 -0
- package/vocab/manifest.json +1 -0
- package/vocab/maven.json +1 -0
- package/vocab/models.json +1 -0
- package/vocab/npm.json +1 -0
- package/vocab/programs.json +1 -0
- package/vocab/pypi.json +1 -0
- package/vocab/rubygems.json +1 -0
- package/dist/commands/deploy-input.d.ts +0 -35
- package/dist/commands/deploy-input.js +0 -107
- package/dist/commands/deploy.d.ts +0 -22
- package/dist/commands/deploy.js +0 -120
- package/dist/commands/status.d.ts +0 -7
- package/dist/commands/status.js +0 -18
- package/dist/commands/test-charge.d.ts +0 -11
- package/dist/commands/test-charge.js +0 -34
- package/dist/journal.d.ts +0 -26
- package/dist/journal.js +0 -58
- package/parity/deploy-plans/paid-api.json +0 -34
- package/parity/deploy-plans/per-task-agent.json +0 -34
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Redactor } from './redact.js';
|
|
2
|
+
import type { ToolManifest, ToolPackage, ToolPull } from './schema.js';
|
|
3
|
+
/** The registry's own normalization (spec Section 1.3): npm lowercases, PyPI folds `-`, `_`, and `.`, crates.io folds `-` and `_`. */
|
|
4
|
+
export declare const NORMALIZE: ReadonlyMap<string, (name: string) => string>;
|
|
5
|
+
/** A name in an ecosystem of the vocabulary, spelled as the registry spells it; null when it is not shaped like one. */
|
|
6
|
+
export declare const resolveName: (ecosystem: string, name: string) => string | null;
|
|
7
|
+
/** One pull as the `package` and `packageVersion` it names, or null when it names no package. */
|
|
8
|
+
export declare const resolvePull: (pull: ToolPull) => Omit<ToolPackage, "first"> | null;
|
|
9
|
+
/**
|
|
10
|
+
* The packages one call installs or runs, each marked `first` when no earlier call of the transcript file named that package at that version. `seen` is the
|
|
11
|
+
* file's running set, so the caller passes the same one for every call in order. A version that fails on first try still counts: the mark is made
|
|
12
|
+
* from the command, before its result is known.
|
|
13
|
+
*/
|
|
14
|
+
export declare const packagesOf: (pulls: readonly ToolPull[], seen: Set<string>) => ToolPackage[];
|
|
15
|
+
export type ManifestFile = ToolManifest['file'];
|
|
16
|
+
/**
|
|
17
|
+
* The manifest a path names, by its basename: `requirements-dev.txt` is a requirements file, `Cargo.toml` is exact. A path with a `node_modules`, `vendor`, `site-packages`,
|
|
18
|
+
* `.venv`, `venv`, `.cargo`, or `bower_components` folder (in any case) names none, since that manifest is a dependency's own. Not covered: pyproject.toml, go.mod, Gemfile, pom.xml.
|
|
19
|
+
*/
|
|
20
|
+
export declare const manifestFileOf: (path: string) => ManifestFile | null;
|
|
21
|
+
/**
|
|
22
|
+
* The packages a manifest text names, as `<ecosystem>:<name>`, each once, sorted. A name the redactor changes is a secret or a mask and is dropped as it was
|
|
23
|
+
* written, before resolving folds its case: the redactor's rules are case-sensitive, so a folded key would pass them.
|
|
24
|
+
*/
|
|
25
|
+
export declare const manifestNames: (file: ManifestFile, text: string, redact: Redactor) => string[];
|
|
26
|
+
/** The names an edit adds and removes: those only in the new text, and those only in the old. A version change moves neither. */
|
|
27
|
+
export declare const manifestDelta: (file: ManifestFile, before: string, after: string, redact: Redactor) => {
|
|
28
|
+
added: string[];
|
|
29
|
+
removed: string[];
|
|
30
|
+
};
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Packages as a capture stores them (pool vocabulary spec Sections 1.2 and 1.3): a name a transcript shows, resolved to `<ecosystem>:<name>` in the
|
|
2
|
+
// registry's own spelling. Resolving is a check of shape only, since capture asks no registry: a name that cannot be told from a path, a flag, a URL, or a
|
|
3
|
+
// masked word is not a package and stores nothing. Whether the name is public is the projection's question (`projectPackage`), asked at deposit.
|
|
4
|
+
/** The registry's own normalization (spec Section 1.3): npm lowercases, PyPI folds `-`, `_`, and `.`, crates.io folds `-` and `_`. */
|
|
5
|
+
export const NORMALIZE = new Map([
|
|
6
|
+
['npm', (name) => name.toLowerCase()],
|
|
7
|
+
['pypi', (name) => name.toLowerCase().replace(/[-_.]+/g, '-')],
|
|
8
|
+
['crates', (name) => name.toLowerCase().replace(/_/g, '-')],
|
|
9
|
+
['rubygems', (name) => name],
|
|
10
|
+
['homebrew', (name) => name.toLowerCase()],
|
|
11
|
+
]);
|
|
12
|
+
/** A capture ecosystem to the vocabulary's. `skill`, `plugin`, and `mcp` are not package registries and are absent. */
|
|
13
|
+
const ECOSYSTEM_OF = new Map([
|
|
14
|
+
['npm', 'npm'],
|
|
15
|
+
['pypi', 'pypi'],
|
|
16
|
+
['cargo', 'crates'],
|
|
17
|
+
['gem', 'rubygems'],
|
|
18
|
+
['brew', 'homebrew'],
|
|
19
|
+
['go', 'go'],
|
|
20
|
+
]);
|
|
21
|
+
/** What each registry accepts as a name. A scoped npm name is the only one with a slash besides a Go module path, so `owner/repo` (a GitHub shorthand) fails. */
|
|
22
|
+
const NAME_SHAPE = new Map([
|
|
23
|
+
['npm', /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/i],
|
|
24
|
+
['pypi', /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/i],
|
|
25
|
+
['crates', /^[a-z][a-z0-9_-]*$/i],
|
|
26
|
+
['rubygems', /^[a-z0-9][a-z0-9._-]*$/i],
|
|
27
|
+
['homebrew', /^[a-z0-9][a-z0-9@+._-]*$/i],
|
|
28
|
+
['go', /^[a-z0-9.-]+\.[a-z0-9.-]+\/[\w.~/-]+$/i],
|
|
29
|
+
]);
|
|
30
|
+
/** A published version: three or four dotted numbers with an optional pre-release or build tail, and Go's leading `v`. A bare `1` is a range to its registry, and capture does not tell a two-part `2.31` (a short exact pin or a short range) apart, so like a range (`^1.2`, `>=2`), a tag (`latest`), or a mask neither is a version. */
|
|
31
|
+
const EXACT_VERSION = /^v?\d+(?:\.\d+){2,3}(?:[-+][0-9A-Za-z.+-]+)?$/;
|
|
32
|
+
/** A name in an ecosystem of the vocabulary, spelled as the registry spells it; null when it is not shaped like one. */
|
|
33
|
+
export const resolveName = (ecosystem, name) => {
|
|
34
|
+
const shape = NAME_SHAPE.get(ecosystem);
|
|
35
|
+
if (shape === undefined || !shape.test(name))
|
|
36
|
+
return null;
|
|
37
|
+
return `${ecosystem}:${NORMALIZE.get(ecosystem)?.(name) ?? name}`;
|
|
38
|
+
};
|
|
39
|
+
/** One pull as the `package` and `packageVersion` it names, or null when it names no package. */
|
|
40
|
+
export const resolvePull = (pull) => {
|
|
41
|
+
const ecosystem = ECOSYSTEM_OF.get(pull.ecosystem);
|
|
42
|
+
const resolved = ecosystem === undefined ? null : resolveName(ecosystem, pull.name);
|
|
43
|
+
if (resolved === null)
|
|
44
|
+
return null;
|
|
45
|
+
return { package: resolved, packageVersion: pull.version !== null && EXACT_VERSION.test(pull.version) ? pull.version : null };
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The packages one call installs or runs, each marked `first` when no earlier call of the transcript file named that package at that version. `seen` is the
|
|
49
|
+
* file's running set, so the caller passes the same one for every call in order. A version that fails on first try still counts: the mark is made
|
|
50
|
+
* from the command, before its result is known.
|
|
51
|
+
*/
|
|
52
|
+
export const packagesOf = (pulls, seen) => {
|
|
53
|
+
const packages = new Map();
|
|
54
|
+
for (const pull of pulls) {
|
|
55
|
+
const resolved = resolvePull(pull);
|
|
56
|
+
if (resolved === null)
|
|
57
|
+
continue;
|
|
58
|
+
const key = `${resolved.package}\n${resolved.packageVersion ?? ''}`;
|
|
59
|
+
if (packages.has(key))
|
|
60
|
+
continue;
|
|
61
|
+
packages.set(key, { ...resolved, first: !seen.has(key) });
|
|
62
|
+
}
|
|
63
|
+
for (const key of packages.keys())
|
|
64
|
+
seen.add(key);
|
|
65
|
+
return [...packages.values()];
|
|
66
|
+
};
|
|
67
|
+
const MANIFEST_ECOSYSTEM = new Map([
|
|
68
|
+
['package.json', 'npm'],
|
|
69
|
+
['requirements.txt', 'pypi'],
|
|
70
|
+
['Cargo.toml', 'crates'],
|
|
71
|
+
]);
|
|
72
|
+
/** A folder that holds code a project installed: a manifest inside it belongs to a dependency, and the session did not choose it. */
|
|
73
|
+
const DEPENDENCY_FOLDERS = new Set(['node_modules', 'vendor', 'site-packages', '.venv', 'venv', '.cargo', 'bower_components']);
|
|
74
|
+
/**
|
|
75
|
+
* The manifest a path names, by its basename: `requirements-dev.txt` is a requirements file, `Cargo.toml` is exact. A path with a `node_modules`, `vendor`, `site-packages`,
|
|
76
|
+
* `.venv`, `venv`, `.cargo`, or `bower_components` folder (in any case) names none, since that manifest is a dependency's own. Not covered: pyproject.toml, go.mod, Gemfile, pom.xml.
|
|
77
|
+
*/
|
|
78
|
+
export const manifestFileOf = (path) => {
|
|
79
|
+
const parts = path.replace(/\\/g, '/').split('/');
|
|
80
|
+
if (parts.some((part) => DEPENDENCY_FOLDERS.has(part.toLowerCase())))
|
|
81
|
+
return null;
|
|
82
|
+
const base = parts.pop() ?? '';
|
|
83
|
+
if (base === 'package.json' || base === 'Cargo.toml')
|
|
84
|
+
return base;
|
|
85
|
+
return /^requirements(?:[-_.][\w.-]+)?\.txt$/.test(base) ? 'requirements.txt' : null;
|
|
86
|
+
};
|
|
87
|
+
/** A Read result numbers its lines (` 12→text`); an edit's text does not. */
|
|
88
|
+
const withoutLineNumbers = (line) => line.replace(/^\s*\d+(?:→|\t)/, '');
|
|
89
|
+
const NODE_DEPENDENCY_KEYS = new Set(['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']);
|
|
90
|
+
/** A range, a pin, or a tag. A `file:`, `workspace:`, `npm:` alias, URL, or git value names no registry package. */
|
|
91
|
+
const NODE_VERSION = /^(?:[~^<>=]*\s*v?\d[\w.*+-]*|\*|latest|next)(?:\s.*)?$/i;
|
|
92
|
+
/** Keys that hold a version-shaped value and are not dependencies, for text that shows no section header. */
|
|
93
|
+
const NOT_NODE_DEPENDENCIES = new Set(['name', 'description', 'main', 'version', 'node', 'npm', 'yarn', 'pnpm', 'tag']);
|
|
94
|
+
const nodeNames = (lines) => {
|
|
95
|
+
// A whole file opens with `{`, so its top-level values are no dependencies. A fragment stays null until a section header is seen: an edit may hold dependency lines alone.
|
|
96
|
+
let active = lines.find((line) => line.trim() !== '')?.trim() === '{' ? false : null;
|
|
97
|
+
const names = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const section = /^\s*"([^"]+)"\s*:\s*\{\s*$/.exec(line)?.[1];
|
|
100
|
+
if (section !== undefined)
|
|
101
|
+
active = NODE_DEPENDENCY_KEYS.has(section);
|
|
102
|
+
else if (/^\s*\},?\s*$/.test(line))
|
|
103
|
+
active = false;
|
|
104
|
+
else if (active !== false) {
|
|
105
|
+
const entry = /^\s*"([^"]+)"\s*:\s*"([^"]*)"\s*,?\s*$/.exec(line);
|
|
106
|
+
if (entry !== null && NODE_VERSION.test(entry[2] ?? '') && (active === true || !NOT_NODE_DEPENDENCIES.has(entry[1] ?? '')))
|
|
107
|
+
names.push(entry[1] ?? '');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return names;
|
|
111
|
+
};
|
|
112
|
+
/** A line that ends in an archive, before any environment marker, names a file, as a path does. */
|
|
113
|
+
const ARCHIVE = /\.(?:whl|tar\.gz|tgz|tar\.bz2|zip|egg)$/i;
|
|
114
|
+
const pythonNames = (lines) => lines.flatMap((line) => {
|
|
115
|
+
const text = line.replace(/(?:^|\s)#.*$/, '').trim();
|
|
116
|
+
if (text === '' || text.startsWith('-') || text.includes('://') || ARCHIVE.test(text.split(';')[0]?.trim() ?? ''))
|
|
117
|
+
return [];
|
|
118
|
+
const name = /^([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)\s*(?:\[[^\]]*\])?\s*(?:$|[=<>!~;@,])/.exec(text)?.[1];
|
|
119
|
+
return name === undefined ? [] : [name];
|
|
120
|
+
});
|
|
121
|
+
const CARGO_DEPENDENCY_TABLE = /(?:^|\.)(?:dev-|build-)?dependencies$/;
|
|
122
|
+
const CARGO_DEPENDENCY_ENTRY = /(?:^|\.)(?:dev-|build-)?dependencies\.([A-Za-z][\w-]*)$/;
|
|
123
|
+
const CARGO_VERSION = /^[~^<>=]*\s*\d[\w.*+-]*$|^\*$/;
|
|
124
|
+
const NOT_CARGO_DEPENDENCIES = new Set(['version', 'name', 'edition', 'rust-version', 'resolver', 'license', 'authors', 'description', 'readme', 'repository', 'homepage', 'documentation', 'publish', 'opt-level', 'keywords', 'categories', 'exclude', 'include', 'build', 'links']);
|
|
125
|
+
const cargoNames = (lines) => {
|
|
126
|
+
// null until a table header is seen, as for package.json.
|
|
127
|
+
let inDependencies = null;
|
|
128
|
+
// A dependency written as its own table is known by its keys, which follow the header: it waits here until the next header or the end of the text.
|
|
129
|
+
let pending = null;
|
|
130
|
+
const names = [];
|
|
131
|
+
const settle = () => {
|
|
132
|
+
if (pending !== null)
|
|
133
|
+
names.push(pending);
|
|
134
|
+
pending = null;
|
|
135
|
+
};
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
const table = /^\s*\[\[?([^\]]+)\]\]?\s*(?:#.*)?$/.exec(line)?.[1]?.replace(/\s/g, '');
|
|
138
|
+
if (table !== undefined) {
|
|
139
|
+
settle();
|
|
140
|
+
pending = CARGO_DEPENDENCY_ENTRY.exec(table)?.[1] ?? null;
|
|
141
|
+
inDependencies = CARGO_DEPENDENCY_TABLE.test(table);
|
|
142
|
+
}
|
|
143
|
+
else if (pending !== null) {
|
|
144
|
+
// A `path` or `git` key makes the table a source and no registry crate; `package` names the real crate behind an alias.
|
|
145
|
+
if (/^\s*(?:path|git)\s*=/.test(line))
|
|
146
|
+
pending = null;
|
|
147
|
+
else
|
|
148
|
+
pending = /^\s*package\s*=\s*"([^"]+)"/.exec(line)?.[1] ?? pending;
|
|
149
|
+
}
|
|
150
|
+
else if (inDependencies !== false) {
|
|
151
|
+
const shared = /^\s*([A-Za-z][\w-]*)\.workspace\s*=\s*true\b/.exec(line)?.[1];
|
|
152
|
+
const entry = /^\s*([A-Za-z][\w-]*)\s*=\s*(.+?)\s*$/.exec(line);
|
|
153
|
+
// A comment after the closing quote or brace is not part of the value.
|
|
154
|
+
const value = (entry?.[2] ?? '').replace(/(?<=["}])\s+#.*$/, '');
|
|
155
|
+
const text = /^"([^"]*)"$/.exec(value)?.[1];
|
|
156
|
+
// An inline table holding `path` or `git` is a source and no registry crate; `package` names the real crate behind an alias.
|
|
157
|
+
const named = text !== undefined ? CARGO_VERSION.test(text) : /^\{.*\b(?:version|workspace)\s*=/.test(value) && !/\b(?:path|git)\s*=/.test(value);
|
|
158
|
+
const real = /\bpackage\s*=\s*"([^"]+)"/.exec(value)?.[1];
|
|
159
|
+
if (shared !== undefined)
|
|
160
|
+
names.push(shared);
|
|
161
|
+
else if (entry !== null && named && (inDependencies === true || !NOT_CARGO_DEPENDENCIES.has(entry[1] ?? '')))
|
|
162
|
+
names.push(real ?? entry[1] ?? '');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
settle();
|
|
166
|
+
return names;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* The packages a manifest text names, as `<ecosystem>:<name>`, each once, sorted. A name the redactor changes is a secret or a mask and is dropped as it was
|
|
170
|
+
* written, before resolving folds its case: the redactor's rules are case-sensitive, so a folded key would pass them.
|
|
171
|
+
*/
|
|
172
|
+
export const manifestNames = (file, text, redact) => {
|
|
173
|
+
const lines = text.split(/\r?\n/).map(withoutLineNumbers);
|
|
174
|
+
const found = file === 'package.json' ? nodeNames(lines) : file === 'requirements.txt' ? pythonNames(lines) : cargoNames(lines);
|
|
175
|
+
const ecosystem = MANIFEST_ECOSYSTEM.get(file) ?? '';
|
|
176
|
+
return [...new Set(found.filter((name) => redact(name) === name).flatMap((name) => resolveName(ecosystem, name) ?? []))].sort();
|
|
177
|
+
};
|
|
178
|
+
/** The names an edit adds and removes: those only in the new text, and those only in the old. A version change moves neither. */
|
|
179
|
+
export const manifestDelta = (file, before, after, redact) => {
|
|
180
|
+
const was = manifestNames(file, before, redact);
|
|
181
|
+
const now = manifestNames(file, after, redact);
|
|
182
|
+
return { added: now.filter((name) => !was.includes(name)), removed: was.filter((name) => !now.includes(name)) };
|
|
183
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many pending sessions one Stop hook deposit takes before its own. The cap bounds the COUNT, not the time: a hook that gets the lock late can
|
|
3
|
+
* still deposit this many transcripts of any size, so the only time limit is the harness's own ceiling on the hook (30 s, install.ts), and a backlog
|
|
4
|
+
* drains over several hooks (a whole-store deposit takes all).
|
|
5
|
+
*/
|
|
6
|
+
export declare const PENDING_HOOK_CAP = 5;
|
|
7
|
+
/** How many failed deposits a pending entry survives: at this many it is removed and counted as dropped, since a whole-store walk still reaches any transcript on the allowlist. */
|
|
8
|
+
export declare const PENDING_MAX_FAILURES = 5;
|
|
9
|
+
export declare const pendingDir: (storeDir: string) => string;
|
|
10
|
+
export interface PendingEntry {
|
|
11
|
+
/** The file names of this entry that were read (one per deferral of the same transcript); the files of an unreadable entry are not here. */
|
|
12
|
+
names: string[];
|
|
13
|
+
/** The transcript to deposit; undefined for a file that is not a pending entry (it is dropped) or one that could not be read. */
|
|
14
|
+
transcript: string | undefined;
|
|
15
|
+
/** True when no file of the entry could be read: it is kept and counted, never removed. */
|
|
16
|
+
unreadable: boolean;
|
|
17
|
+
/** Failed deposits so far, the highest over the entry's files. */
|
|
18
|
+
failures: number;
|
|
19
|
+
}
|
|
20
|
+
/** Records `transcript` as pending without taking the store lock. The same session twice is two files and one entry. */
|
|
21
|
+
export declare const deferSession: (storeDir: string, transcript: string) => Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* The first `limit` readable pending entries, read: fewest failures first, then name order, and then any unreadable entries met on the way (they
|
|
24
|
+
* never count toward `limit` and never go first, so a file that cannot be read cannot fill the hook's cap or be retried ahead of a real session).
|
|
25
|
+
* A store with no pending folder has none, and a pending folder that cannot be listed is an empty list with one line on stderr, so it never
|
|
26
|
+
* fails the deposit it was read for.
|
|
27
|
+
*/
|
|
28
|
+
export declare const listPending: (storeDir: string, limit: number) => Promise<PendingEntry[]>;
|
|
29
|
+
/** Removes the files of the entries a deposit settled, after its `state.json` was written, so a crash before that leaves them for the next run. */
|
|
30
|
+
export declare const removePending: (storeDir: string, entries: readonly PendingEntry[]) => Promise<void>;
|
|
31
|
+
/** Counts one more failed deposit of `entry`, in the name of its first file. The caller holds the store lock, so no one else renames or removes it. */
|
|
32
|
+
export declare const failPending: (storeDir: string, entry: PendingEntry) => Promise<void>;
|
|
33
|
+
/** Removes the `*.tmp` files a hook died before renaming, once they are older than `staleMs`: the age no live write reaches. */
|
|
34
|
+
export declare const sweepPendingTemps: (storeDir: string, staleMs: number) => Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// The pending list (register row A55): the sessions a Stop hook deposit could not take because another deposit held the store lock.
|
|
2
|
+
//
|
|
3
|
+
// One small file per deferral under `<store>/pending/`, named `<hash of the transcript path>.<uuid>.json` and holding `{"transcript": "<path>"}`:
|
|
4
|
+
// what a later deposit needs to find the transcript, and nothing else (no prompt text, no id, no project name beyond the path). One file per
|
|
5
|
+
// deferral, not one shared list, because the hook that defers must not take the store lock (it just failed to) and two hooks may defer at once:
|
|
6
|
+
// each writes a file name no one else uses. A drain removes only the file names it read, so a session that defers again while a drain runs leaves
|
|
7
|
+
// its newer file behind for the next deposit, and an append racing a drain is never lost. The drain dedupes by the hash, so a session that
|
|
8
|
+
// deferred twice is one entry. Each file is written to a temp name and renamed, so a drain never reads a torn one.
|
|
9
|
+
//
|
|
10
|
+
// A deposit that fails on an entry renames it `<hash>.<uuid>.f<n>.json`, so the number of failures is in the name and ordering the list needs
|
|
11
|
+
// no read of the file: entries with fewer failures come first, and an entry that always fails cannot fill the hook's cap for good.
|
|
12
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
13
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* How many pending sessions one Stop hook deposit takes before its own. The cap bounds the COUNT, not the time: a hook that gets the lock late can
|
|
17
|
+
* still deposit this many transcripts of any size, so the only time limit is the harness's own ceiling on the hook (30 s, install.ts), and a backlog
|
|
18
|
+
* drains over several hooks (a whole-store deposit takes all).
|
|
19
|
+
*/
|
|
20
|
+
export const PENDING_HOOK_CAP = 5;
|
|
21
|
+
/** How many failed deposits a pending entry survives: at this many it is removed and counted as dropped, since a whole-store walk still reaches any transcript on the allowlist. */
|
|
22
|
+
export const PENDING_MAX_FAILURES = 5;
|
|
23
|
+
/** One line on stderr for a pending-list file operation that failed and was skipped: the deposit that met it goes on. */
|
|
24
|
+
const warn = (what, error) => {
|
|
25
|
+
process.stderr.write(`oars: ${what}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
26
|
+
};
|
|
27
|
+
export const pendingDir = (storeDir) => join(storeDir, 'pending');
|
|
28
|
+
const hashOf = (transcript) => createHash('sha256').update(transcript).digest('hex').slice(0, 32);
|
|
29
|
+
/** The part of a file name before the first dot: the hash, which is what makes two deferrals one entry. */
|
|
30
|
+
const keyOf = (name) => name.slice(0, name.indexOf('.'));
|
|
31
|
+
const FAILED = /\.f(\d+)\.json$/;
|
|
32
|
+
const failuresOf = (name) => Number(FAILED.exec(name)?.[1] ?? 0);
|
|
33
|
+
/** Records `transcript` as pending without taking the store lock. The same session twice is two files and one entry. */
|
|
34
|
+
export const deferSession = async (storeDir, transcript) => {
|
|
35
|
+
const dir = pendingDir(storeDir);
|
|
36
|
+
await mkdir(dir, { recursive: true });
|
|
37
|
+
const temp = join(dir, `${randomUUID()}.tmp`);
|
|
38
|
+
await writeFile(temp, JSON.stringify({ transcript }), { mode: 0o600 });
|
|
39
|
+
await rename(temp, join(dir, `${hashOf(transcript)}.${randomUUID()}.json`));
|
|
40
|
+
};
|
|
41
|
+
const transcriptOf = (text) => {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(text);
|
|
44
|
+
const transcript = typeof parsed === 'object' && parsed !== null ? Reflect.get(parsed, 'transcript') : undefined;
|
|
45
|
+
return typeof transcript === 'string' && transcript !== '' ? transcript : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const entryOf = async (storeDir, names) => {
|
|
52
|
+
const failures = Math.max(...names.map(failuresOf));
|
|
53
|
+
const read = [];
|
|
54
|
+
let transcript;
|
|
55
|
+
for (const name of names) {
|
|
56
|
+
try {
|
|
57
|
+
const found = transcriptOf(await readFile(join(pendingDir(storeDir), name), 'utf8'));
|
|
58
|
+
read.push(name);
|
|
59
|
+
transcript ??= found;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A file that cannot be read stays where it is: it is not in `names`, so a drain never removes it.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { names: read, transcript, unreadable: read.length === 0, failures };
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The first `limit` readable pending entries, read: fewest failures first, then name order, and then any unreadable entries met on the way (they
|
|
69
|
+
* never count toward `limit` and never go first, so a file that cannot be read cannot fill the hook's cap or be retried ahead of a real session).
|
|
70
|
+
* A store with no pending folder has none, and a pending folder that cannot be listed is an empty list with one line on stderr, so it never
|
|
71
|
+
* fails the deposit it was read for.
|
|
72
|
+
*/
|
|
73
|
+
export const listPending = async (storeDir, limit) => {
|
|
74
|
+
let names;
|
|
75
|
+
try {
|
|
76
|
+
names = (await readdir(pendingDir(storeDir))).filter((name) => name.endsWith('.json'));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
80
|
+
return [];
|
|
81
|
+
warn('the pending list could not be read and is skipped', error);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const groups = new Map();
|
|
85
|
+
for (const name of names.sort())
|
|
86
|
+
groups.set(keyOf(name), [...(groups.get(keyOf(name)) ?? []), name]);
|
|
87
|
+
const ordered = [...groups.values()].map((group) => ({ group, failures: Math.max(...group.map(failuresOf)) })).sort((a, b) => a.failures - b.failures);
|
|
88
|
+
const readable = [];
|
|
89
|
+
const unreadable = [];
|
|
90
|
+
// Read in batches of as many entries as are still wanted, so an unreadable one is read past rather than counted.
|
|
91
|
+
for (let next = 0; next < ordered.length && readable.length < limit;) {
|
|
92
|
+
const batch = ordered.slice(next, next + Math.max(1, limit - readable.length));
|
|
93
|
+
next += batch.length;
|
|
94
|
+
for (const entry of await Promise.all(batch.map((item) => entryOf(storeDir, item.group))))
|
|
95
|
+
(entry.unreadable ? unreadable : readable).push(entry);
|
|
96
|
+
}
|
|
97
|
+
return [...readable, ...unreadable];
|
|
98
|
+
};
|
|
99
|
+
/** Removes the files of the entries a deposit settled, after its `state.json` was written, so a crash before that leaves them for the next run. */
|
|
100
|
+
export const removePending = async (storeDir, entries) => {
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
for (const name of entry.names) {
|
|
103
|
+
try {
|
|
104
|
+
await rm(join(pendingDir(storeDir), name), { force: true });
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
warn(`a settled pending file could not be removed and is left for the next run: ${name}`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
/** Counts one more failed deposit of `entry`, in the name of its first file. The caller holds the store lock, so no one else renames or removes it. */
|
|
113
|
+
export const failPending = async (storeDir, entry) => {
|
|
114
|
+
const failures = entry.failures + 1;
|
|
115
|
+
const first = entry.names[0];
|
|
116
|
+
if (first !== undefined) {
|
|
117
|
+
const base = first.replace(FAILED, '.json').replace(/\.json$/, `.f${String(failures)}.json`);
|
|
118
|
+
try {
|
|
119
|
+
await rename(join(pendingDir(storeDir), first), join(pendingDir(storeDir), base));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
warn(`a failed pending file could not be renamed and is left for the next run: ${first}`, error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/** Removes the `*.tmp` files a hook died before renaming, once they are older than `staleMs`: the age no live write reaches. */
|
|
127
|
+
export const sweepPendingTemps = async (storeDir, staleMs) => {
|
|
128
|
+
const dir = pendingDir(storeDir);
|
|
129
|
+
const names = await readdir(dir).catch(() => []);
|
|
130
|
+
for (const name of names.filter((entry) => entry.endsWith('.tmp'))) {
|
|
131
|
+
const age = await stat(join(dir, name)).then((info) => Date.now() - info.mtimeMs, () => 0);
|
|
132
|
+
if (age <= staleMs)
|
|
133
|
+
continue;
|
|
134
|
+
try {
|
|
135
|
+
await rm(join(dir, name), { force: true });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
// Housekeeping: a leftover this sweep cannot remove (a folder named like one, or a file Windows refuses) costs a line, never the deposit.
|
|
139
|
+
warn(`a stale pending temp file could not be removed and is left in place: ${name}`, error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Outcome, RunDeps } from '../cli.js';
|
|
2
|
+
import { type Outgoing } from './pool-print.js';
|
|
3
|
+
export declare const POOL_PROVIDERS: readonly ["github", "google"];
|
|
4
|
+
/** `--server`, else `OARS_POOL_URL`; never a default. A token and a session's shape cross this connection, so plain http goes only to this machine. */
|
|
5
|
+
export declare const poolServer: (env: NodeJS.ProcessEnv, step: string, named?: string) => string;
|
|
6
|
+
export interface Reply {
|
|
7
|
+
status: number;
|
|
8
|
+
body: unknown;
|
|
9
|
+
headers: Headers;
|
|
10
|
+
}
|
|
11
|
+
/** How a request leaves this machine. The command line passes none and so sends no batch; the lock in `deposit --send` is tested with one. */
|
|
12
|
+
export type Sender = (request: Outgoing) => Promise<Reply>;
|
|
13
|
+
/** The sender a real send would use: the request goes out over the network as built. The command line does not pass it (sending waits on the covenant); the loopback test does, to read what reaches the socket. */
|
|
14
|
+
export declare const networkSender: (deps: RunDeps, step: string) => Sender;
|
|
15
|
+
/**
|
|
16
|
+
* `oars pool login` (spec Section 1.4): the device flow runs here, against the provider, and the pool sees only the provider's token, once, and answers its own credential. Every request prints before it is sent, with the device code, the provider's token,
|
|
17
|
+
* and the credential masked as `<credential: n bytes>`. A client ID, scope, grant type, and Google's `client_secret` print as they are: none is secret.
|
|
18
|
+
*/
|
|
19
|
+
export declare const poolLogin: (deps: RunDeps, options: {
|
|
20
|
+
server?: string | undefined;
|
|
21
|
+
provider?: string | undefined;
|
|
22
|
+
}) => Promise<Outcome>;
|
|
23
|
+
/**
|
|
24
|
+
* `oars pool deposit`: every session in the store that passes both session gates is built into a canonical batch in the outbox, and the command stops: nothing is sent. The gates run before any projection does: the folder
|
|
25
|
+
* allowlist, then the exclusion check. A session that fails either, or has no raw to scan, is counted and never built. A session is built only once it has ended: no transcript of it, main or subagent, has gained a line (the later of its capture
|
|
26
|
+
* and its raw file's last write) for `session_idle_hours` (6 hours). One that has not is counted under `notEnded`, and any batch of it from an earlier pass is withdrawn. `--send <hash>` is the lock in `sendBatch`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const poolDeposit: (deps: RunDeps, options: {
|
|
29
|
+
server?: string | undefined;
|
|
30
|
+
acceptCovenant?: string | undefined;
|
|
31
|
+
send?: string | undefined;
|
|
32
|
+
via?: Sender | undefined;
|
|
33
|
+
}) => Promise<Outcome>;
|
|
34
|
+
/** `oars pool outbox`: prints each batch as it would be sent, the request line and every header included, with the bearer credential as its size and nothing else masked, then its SHA-256, and records that it was shown. `--explain` adds each field's rule. */
|
|
35
|
+
export declare const poolOutbox: (deps: RunDeps, options: {
|
|
36
|
+
server?: string | undefined;
|
|
37
|
+
explain?: boolean | undefined;
|
|
38
|
+
}) => Promise<Outcome>;
|
|
39
|
+
/** `oars pool fetch`: one harness's signed bundle at one version, kept only once the key pinned at login has verified it. It keeps the signed form, so the replica can be checked again later, and the ETag, so the next fetch asks only whether it changed. The request prints before it is sent. */
|
|
40
|
+
export declare const poolFetch: (deps: RunDeps, options: {
|
|
41
|
+
server?: string | undefined;
|
|
42
|
+
harness?: string | undefined;
|
|
43
|
+
harnessVersion: string;
|
|
44
|
+
}) => Promise<Outcome>;
|