@reticlehq/server 3.2.0 → 3.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/SKILL.md +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The intent ledger on disk: one directory per subject, and a flow's name IS its subject.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* .reticle/intent/index.json every id, its subject and one line — derived
|
|
5
|
+
* .reticle/intent/<subject>/intent.json the full records for that subject
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The ONE place intents are stored. `IntentStore` is the domain API over it (declare, bind,
|
|
8
|
+
* discharge) and never writes a file of its own; `reticle_intent`'s record/get/index/subject read
|
|
9
|
+
* this directly. Two stores kept in two files is how an intent written through one action became
|
|
10
|
+
* invisible to the other, and an agent told its own intent does not exist concludes it does not.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Git-checked on purpose: a human sees in review when an intent is narrowed to match what was easy
|
|
13
|
+
* to prove. Writes are byte-stable, and a write that changes nothing writes nothing, because a ledger
|
|
14
|
+
* that churns on every run is one whose diffs nobody reads.
|
|
14
15
|
*
|
|
15
|
-
* ## Migration
|
|
16
|
+
* ## Migration happens on the first write, and it removes what it moved
|
|
16
17
|
*
|
|
17
|
-
* `.reticle/intent.json`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is a
|
|
18
|
+
* The old single `.reticle/intent.json` (and any interim `intent/<subject>.json`) is read until then,
|
|
19
|
+
* so nothing is invisible before the move. The first write folds every record into its directory and
|
|
20
|
+
* deletes the old files, so the change shows up in one reviewable diff. A legacy file that does not
|
|
21
|
+
* PARSE is never deleted: a hand-merge leaves conflict markers, it reads as empty, and removing it
|
|
22
|
+
* would destroy the only copy of every intent inside.
|
|
21
23
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import { ReticleDir } from '@reticlehq/core';
|
|
24
|
+
import { IntentFileSchema, IntentState, amendIntent, isActionLabel, } from '@reticlehq/core/artifacts';
|
|
25
|
+
import { IntentDir, ReticleDir } from '@reticlehq/core';
|
|
24
26
|
import { reticleDirPaths } from '../project/dir/reticle-dir.js';
|
|
25
27
|
import { withFileLock } from '../project/file-lock.js';
|
|
26
28
|
import { subjectFor } from './intent-subject.js';
|
|
27
|
-
import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom,
|
|
28
|
-
|
|
29
|
-
const
|
|
29
|
+
import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom, statusFromState, } from './intent-shard.js';
|
|
30
|
+
import { writeFileAtomic } from '../project/fs/write-atomic.js';
|
|
31
|
+
const JSON_SUFFIX = '.json';
|
|
32
|
+
/** Beside an unparseable shard: its bytes, kept before it is rewritten. See `#shardForWrite`. */
|
|
33
|
+
const UNREADABLE_SUFFIX = '.unreadable-';
|
|
34
|
+
/** The intent fields a record is compared on — `updatedAt` alone changing is not a change. */
|
|
35
|
+
const comparable = (record) => serialise({ ...record, updatedAt: 0 });
|
|
30
36
|
export class IntentShardStore {
|
|
31
37
|
#fs;
|
|
32
38
|
#root;
|
|
@@ -40,175 +46,281 @@ export class IntentShardStore {
|
|
|
40
46
|
return `${reticleDirPaths(this.#root).root}/${ReticleDir.INTENT_SUBDIR}`;
|
|
41
47
|
}
|
|
42
48
|
#indexPath() {
|
|
43
|
-
return `${this.#dir()}/${INDEX_FILE}`;
|
|
49
|
+
return `${this.#dir()}/${IntentDir.INDEX_FILE}`;
|
|
44
50
|
}
|
|
45
51
|
#shardPath(subject) {
|
|
46
|
-
return `${this.#dir()}/${subject}
|
|
52
|
+
return `${this.#dir()}/${subject}/${IntentDir.SHARD_FILE}`;
|
|
47
53
|
}
|
|
48
|
-
/**
|
|
49
|
-
async #
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
/** Parse one legacy file, remembering whether it parsed — only a parsed file may be deleted. */
|
|
55
|
+
async #readLegacy(pathOf, toRecords) {
|
|
56
|
+
let path = '';
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
// Resolved INSIDE the try: with no known project root there is no path, and that must read as
|
|
60
|
+
// "no legacy file" — the old store failed soft here, and a caller only asking what is still
|
|
61
|
+
// open must not be taken down by it.
|
|
62
|
+
path = pathOf();
|
|
63
|
+
text = await this.#fs.readFile(path);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { path, records: [], parsed: true };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return { path, records: toRecords(JSON.parse(text)), parsed: true };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { path, records: [], parsed: false };
|
|
73
|
+
}
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
/** The old single file, and any interim `intent/<subject>.json` from before directories. */
|
|
76
|
+
async #legacySources() {
|
|
77
|
+
const flat = await this.#readLegacy(() => reticleDirPaths(this.#root).intent, (raw) => Object.values(IntentFileSchema.parse(raw).intents).map(recordFromIntent));
|
|
78
|
+
const interim = await Promise.all((await this.#entries())
|
|
79
|
+
.filter((e) => e.endsWith(JSON_SUFFIX) && IntentDir.INDEX_FILE !== e)
|
|
80
|
+
.map((e) => this.#readLegacy(() => `${this.#dir()}/${e}`, (raw) => Object.values(IntentShardSchema.parse(raw).intents))));
|
|
81
|
+
return [flat, ...interim].filter((l) => l.records.length > 0 || !l.parsed);
|
|
82
|
+
}
|
|
83
|
+
async #entries() {
|
|
84
|
+
try {
|
|
85
|
+
return await this.#fs.readdir(this.#dir());
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
55
90
|
}
|
|
56
91
|
/**
|
|
57
92
|
* Which subjects exist, read from the DIRECTORY rather than the index.
|
|
58
93
|
*
|
|
59
|
-
* The index
|
|
60
|
-
*
|
|
61
|
-
* the
|
|
62
|
-
* mention it yet. Listing the directory also makes the index self-healing: delete it and the next
|
|
63
|
-
* read rebuilds it from what is actually there.
|
|
94
|
+
* The index is derived from the shards, so discovering shards through it would be circular: a new
|
|
95
|
+
* subject was once invisible to the very write that created it. Listing the directory also makes
|
|
96
|
+
* the index self-healing — delete it and the next write rebuilds it from what is actually there.
|
|
64
97
|
*/
|
|
65
98
|
async #subjectsOnDisk() {
|
|
99
|
+
const subjects = [];
|
|
100
|
+
for (const entry of await this.#entries()) {
|
|
101
|
+
if (entry.endsWith(JSON_SUFFIX))
|
|
102
|
+
continue;
|
|
103
|
+
if (await this.#fs.exists(this.#shardPath(entry)))
|
|
104
|
+
subjects.push(entry);
|
|
105
|
+
}
|
|
106
|
+
return subjects;
|
|
107
|
+
}
|
|
108
|
+
async #readShard(subject) {
|
|
66
109
|
try {
|
|
67
|
-
|
|
68
|
-
return entries
|
|
69
|
-
.filter((e) => e.endsWith(SHARD_SUFFIX) && INDEX_FILE !== e)
|
|
70
|
-
.map((e) => e.slice(0, -SHARD_SUFFIX.length));
|
|
110
|
+
return IntentShardSchema.parse(JSON.parse(await this.#fs.readFile(this.#shardPath(subject))));
|
|
71
111
|
}
|
|
72
112
|
catch {
|
|
73
|
-
|
|
74
|
-
return [];
|
|
113
|
+
return emptyShard(subject);
|
|
75
114
|
}
|
|
76
115
|
}
|
|
77
116
|
/**
|
|
78
|
-
*
|
|
117
|
+
* A shard about to be rewritten, read so that nothing in it can be lost.
|
|
79
118
|
*
|
|
80
|
-
*
|
|
81
|
-
* and
|
|
119
|
+
* `#readShard` answers "what is recorded here" and treats an unparseable shard as empty, which is
|
|
120
|
+
* right for reading and wrong for rewriting: the next write put "empty plus one record" over a file
|
|
121
|
+
* that held every other intent for that subject (#994). An unreadable shard's bytes are copied
|
|
122
|
+
* beside it, untouched, before it is replaced, the same rule migration keeps for a legacy file it
|
|
123
|
+
* cannot parse. A missing shard is simply empty.
|
|
82
124
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
async #shardForWrite(subject) {
|
|
126
|
+
const path = this.#shardPath(subject);
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = await this.#fs.readFile(path);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return emptyShard(subject);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return IntentShardSchema.parse(JSON.parse(text));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
await writeFileAtomic(this.#fs, `${path}${UNREADABLE_SUFFIX}${String(this.#clock.now())}`, text);
|
|
139
|
+
return emptyShard(subject);
|
|
140
|
+
}
|
|
94
141
|
}
|
|
95
|
-
async #
|
|
142
|
+
async #storedRecords() {
|
|
96
143
|
const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
|
|
144
|
+
return shards.flatMap((s) => Object.values(s.intents));
|
|
145
|
+
}
|
|
146
|
+
/** Every record: the directories, plus any legacy intent not yet moved. Directories win. */
|
|
147
|
+
async all() {
|
|
97
148
|
const merged = new Map();
|
|
98
|
-
for (const
|
|
99
|
-
|
|
100
|
-
for (const shard of shards) {
|
|
101
|
-
for (const record of Object.values(shard.intents))
|
|
149
|
+
for (const legacy of await this.#legacySources()) {
|
|
150
|
+
for (const record of legacy.records)
|
|
102
151
|
merged.set(record.id, record);
|
|
103
152
|
}
|
|
104
|
-
|
|
153
|
+
for (const record of await this.#storedRecords())
|
|
154
|
+
merged.set(record.id, record);
|
|
155
|
+
return [...merged.values()].sort((a, b) => a.declaredAt - b.declaredAt || a.id.localeCompare(b.id));
|
|
105
156
|
}
|
|
106
|
-
/**
|
|
107
|
-
* The cheap read: one line per intent, and the call an agent makes at the start of a session.
|
|
108
|
-
*
|
|
109
|
-
* Includes anything still only in the legacy flat file, so the very first call on an unmigrated
|
|
110
|
-
* project already answers completely — which is what makes the migration invisible rather than a
|
|
111
|
-
* step somebody has to remember to run.
|
|
112
|
-
*/
|
|
157
|
+
/** The cheap read: one line per intent. Derived from disk, never from the stored copy. */
|
|
113
158
|
async index() {
|
|
114
|
-
|
|
115
|
-
// readers outside this process; inside it, trusting it would let a stale one hide a real record.
|
|
116
|
-
return indexFrom(shardsFrom(await this.#all()));
|
|
117
|
-
}
|
|
118
|
-
/** Records already in shards, ignoring the legacy file — what migration diffs against. */
|
|
119
|
-
async #storedRecords() {
|
|
120
|
-
const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
|
|
121
|
-
return shards.flatMap((s) => Object.values(s.intents));
|
|
159
|
+
return indexFrom(shardsFrom(await this.all()));
|
|
122
160
|
}
|
|
123
161
|
/** Every record for one subject — the working read, once an agent knows what it is touching. */
|
|
124
162
|
async subject(name) {
|
|
125
|
-
|
|
126
|
-
return all.filter((r) => name === r.subject);
|
|
163
|
+
return (await this.all()).filter((r) => name === r.subject);
|
|
127
164
|
}
|
|
128
165
|
/** One record by id, or null. */
|
|
129
166
|
async get(id) {
|
|
130
|
-
|
|
131
|
-
return all.find((r) => id === r.id) ?? null;
|
|
167
|
+
return (await this.all()).find((r) => id === r.id) ?? null;
|
|
132
168
|
}
|
|
133
169
|
/**
|
|
134
|
-
*
|
|
170
|
+
* Create or change ONE intent atomically: read, transform, write, under the ledger's lock.
|
|
135
171
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
172
|
+
* `transform` returns the next intent, or undefined for "leave it as it is". A result equal to
|
|
173
|
+
* what is stored writes nothing. Returns the stored record, or undefined when nothing was there
|
|
174
|
+
* and nothing was created.
|
|
139
175
|
*/
|
|
140
|
-
async
|
|
176
|
+
async upsert(id, transform, facts = {}) {
|
|
141
177
|
return withFileLock(this.#indexPath(), async () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
subject,
|
|
154
|
-
status: IntentStatus.PROPOSED,
|
|
155
|
-
}),
|
|
156
|
-
statement: input.statement,
|
|
157
|
-
subject,
|
|
158
|
-
status: input.status ?? existing?.status ?? IntentStatus.PROPOSED,
|
|
159
|
-
updatedAt: now,
|
|
160
|
-
...(input.why === undefined ? {} : { why: input.why }),
|
|
161
|
-
...(input.source === undefined ? {} : { source: input.source }),
|
|
162
|
-
...(input.surface === undefined ? {} : { surface: input.surface }),
|
|
163
|
-
...(input.binding === undefined ? {} : { binding: input.binding }),
|
|
164
|
-
};
|
|
165
|
-
await this.#write(next, existing?.subject);
|
|
166
|
-
return next;
|
|
178
|
+
await this.#migrateLegacy();
|
|
179
|
+
await this.#retireLabels();
|
|
180
|
+
const existing = (await this.#storedRecords()).find((r) => id === r.id);
|
|
181
|
+
const next = transform(existing);
|
|
182
|
+
if (next === undefined)
|
|
183
|
+
return existing;
|
|
184
|
+
const record = this.#recordFrom(amendIntent(existing, next), existing, facts);
|
|
185
|
+
if (existing !== undefined && comparable(existing) === comparable(record))
|
|
186
|
+
return existing;
|
|
187
|
+
await this.#write(record, existing?.subject);
|
|
188
|
+
return record;
|
|
167
189
|
});
|
|
168
190
|
}
|
|
169
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The ledger-level fields of a record: where it lives and how settled it is.
|
|
193
|
+
*
|
|
194
|
+
* A flow's name IS its subject, so an intent a flow claims moves into that flow's directory.
|
|
195
|
+
* Otherwise an explicit subject wins, then the one it already had, then inference.
|
|
196
|
+
*/
|
|
197
|
+
#recordFrom(next, existing, facts) {
|
|
198
|
+
const flow = next.surface?.flow;
|
|
199
|
+
const subject = flow !== undefined
|
|
200
|
+
? subjectFor({ surface: { flow } })
|
|
201
|
+
: (facts.subject ??
|
|
202
|
+
existing?.subject ??
|
|
203
|
+
subjectFor({ surface: next.surface, binding: next.binding }));
|
|
204
|
+
const why = facts.why ?? existing?.why;
|
|
205
|
+
const source = facts.source ?? existing?.source;
|
|
206
|
+
return {
|
|
207
|
+
...existing,
|
|
208
|
+
...next,
|
|
209
|
+
subject,
|
|
210
|
+
status: this.#statusOf(next, existing, facts.status),
|
|
211
|
+
updatedAt: this.#clock.now(),
|
|
212
|
+
...(why === undefined ? {} : { why }),
|
|
213
|
+
...(source === undefined ? {} : { source }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* How settled the record is. A verdict proving it makes it proved; losing that proof (the words
|
|
218
|
+
* changed, a different check was bound) steps it back rather than leaving a stale "proved".
|
|
219
|
+
*/
|
|
220
|
+
#statusOf(next, existing, asked) {
|
|
221
|
+
if (asked !== undefined)
|
|
222
|
+
return asked;
|
|
223
|
+
if (IntentState.PROVED === next.state)
|
|
224
|
+
return IntentStatus.PROVED;
|
|
225
|
+
if (IntentStatus.PROVED === existing?.status)
|
|
226
|
+
return statusFromState(next.state);
|
|
227
|
+
return existing?.status ?? IntentStatus.PROPOSED;
|
|
228
|
+
}
|
|
229
|
+
/** Write one intent through `reticle_intent { action: "record" }`, merging onto what is stored. */
|
|
230
|
+
async record(input) {
|
|
231
|
+
const now = this.#clock.now();
|
|
232
|
+
const stored = await this.upsert(input.id, (existing) => ({
|
|
233
|
+
...(existing ?? { id: input.id, state: IntentState.DECLARED, declaredAt: now }),
|
|
234
|
+
statement: input.statement,
|
|
235
|
+
...(input.surface === undefined ? {} : { surface: input.surface }),
|
|
236
|
+
...(input.binding === undefined ? {} : { binding: input.binding }),
|
|
237
|
+
}), { subject: input.subject, status: input.status, why: input.why, source: input.source });
|
|
238
|
+
// upsert only returns undefined when the transform declines, and this one never does.
|
|
239
|
+
return stored;
|
|
240
|
+
}
|
|
241
|
+
/** Persist one record, rewriting its shard and the index, and removing it from its old shard. */
|
|
170
242
|
async #write(record, previousSubject) {
|
|
171
|
-
await this.#fs.mkdir(this.#dir());
|
|
172
243
|
if (previousSubject !== undefined && record.subject !== previousSubject) {
|
|
173
|
-
const old = await this.#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await this.#fs.writeFile(this.#shardPath(previousSubject), serialise({ ...old, intents: rest }));
|
|
177
|
-
}
|
|
244
|
+
const old = await this.#shardForWrite(previousSubject);
|
|
245
|
+
const { [record.id]: _moved, ...rest } = old.intents;
|
|
246
|
+
await this.#putShard({ ...old, intents: rest });
|
|
178
247
|
}
|
|
179
|
-
const shard = await this.#
|
|
248
|
+
const shard = await this.#shardForWrite(record.subject);
|
|
180
249
|
shard.intents[record.id] = record;
|
|
181
|
-
await this.#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
250
|
+
await this.#putShard(shard);
|
|
251
|
+
await this.#writeIndex();
|
|
252
|
+
}
|
|
253
|
+
/** Write a shard, or remove it when it no longer holds anything. */
|
|
254
|
+
async #putShard(shard) {
|
|
255
|
+
const path = this.#shardPath(shard.subject);
|
|
256
|
+
if (0 === Object.keys(shard.intents).length) {
|
|
257
|
+
await this.#fs.rm(path).catch(() => undefined);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Checked against the schema the reader applies, BEFORE it reaches the disk: a record the
|
|
261
|
+
// reader would refuse makes the whole shard unreadable, and every other intent in it with it.
|
|
262
|
+
// Refused here, loudly, where the one bad write is still the caller's to fix (#1020).
|
|
263
|
+
const valid = IntentShardSchema.parse(shard);
|
|
264
|
+
await this.#fs.mkdir(`${this.#dir()}/${shard.subject}`);
|
|
265
|
+
await writeFileAtomic(this.#fs, path, serialise(valid));
|
|
266
|
+
}
|
|
267
|
+
async #writeIndex() {
|
|
268
|
+
await this.#fs.mkdir(this.#dir());
|
|
269
|
+
const index = indexFrom(shardsFrom(await this.#storedRecords()));
|
|
270
|
+
await writeFileAtomic(this.#fs, this.#indexPath(), serialise(index));
|
|
186
271
|
}
|
|
187
272
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
|
|
191
|
-
* migration that removes its own source cannot be checked afterwards.
|
|
273
|
+
* Move every legacy record into its directory, then remove the legacy files that parsed.
|
|
274
|
+
* Called under the lock by every write, so the first write after an upgrade is the migration.
|
|
192
275
|
*/
|
|
276
|
+
async #migrateLegacy() {
|
|
277
|
+
const sources = await this.#legacySources();
|
|
278
|
+
if (0 === sources.length)
|
|
279
|
+
return { migrated: [] };
|
|
280
|
+
const known = new Set((await this.#storedRecords()).map((r) => r.id));
|
|
281
|
+
const incoming = sources.flatMap((s) => s.records).filter((r) => !known.has(r.id));
|
|
282
|
+
for (const shard of shardsFrom(incoming)) {
|
|
283
|
+
const current = await this.#shardForWrite(shard.subject);
|
|
284
|
+
await this.#putShard({ ...current, intents: { ...shard.intents, ...current.intents } });
|
|
285
|
+
}
|
|
286
|
+
for (const source of sources) {
|
|
287
|
+
if (source.parsed)
|
|
288
|
+
await this.#fs.rm(source.path).catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
await this.#writeIndex();
|
|
291
|
+
return { migrated: incoming };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Mark every stored step label stale: "click button \"Cancel\"" describes what was DONE, not a
|
|
295
|
+
* rule. Kept rather than deleted, because removing a record loses the fact it was ever written.
|
|
296
|
+
* Runs with every write, and writes nothing once there is nothing left to retire.
|
|
297
|
+
*/
|
|
298
|
+
async #retireLabels() {
|
|
299
|
+
const labels = (await this.#storedRecords()).filter((r) => IntentStatus.STALE !== r.status && isActionLabel(r.statement));
|
|
300
|
+
if (0 === labels.length)
|
|
301
|
+
return 0;
|
|
302
|
+
for (const subject of new Set(labels.map((r) => r.subject))) {
|
|
303
|
+
const shard = await this.#shardForWrite(subject);
|
|
304
|
+
for (const record of labels.filter((r) => subject === r.subject)) {
|
|
305
|
+
shard.intents[record.id] = {
|
|
306
|
+
...record,
|
|
307
|
+
status: IntentStatus.STALE,
|
|
308
|
+
updatedAt: this.#clock.now(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
await this.#putShard(shard);
|
|
312
|
+
}
|
|
313
|
+
await this.#writeIndex();
|
|
314
|
+
return labels.length;
|
|
315
|
+
}
|
|
316
|
+
/** `reticle_intent { action: "migrate" }`: do now what the next write would do anyway. */
|
|
193
317
|
async migrate() {
|
|
194
318
|
return withFileLock(this.#indexPath(), async () => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
return { migrated: 0, subjects: [] };
|
|
198
|
-
const stored = await this.#storedRecords();
|
|
199
|
-
const known = new Set(stored.map((r) => r.id));
|
|
200
|
-
const incoming = [...legacy.values()].filter((r) => !known.has(r.id));
|
|
201
|
-
if (0 === incoming.length)
|
|
202
|
-
return { migrated: 0, subjects: [] };
|
|
203
|
-
const shards = shardsFrom([...stored, ...incoming]);
|
|
204
|
-
await this.#fs.mkdir(this.#dir());
|
|
205
|
-
for (const shard of shards) {
|
|
206
|
-
await this.#fs.writeFile(this.#shardPath(shard.subject), serialise(shard));
|
|
207
|
-
}
|
|
208
|
-
await this.#fs.writeFile(this.#indexPath(), serialise(indexFrom(shards)));
|
|
319
|
+
const { migrated } = await this.#migrateLegacy();
|
|
320
|
+
await this.#retireLabels();
|
|
209
321
|
return {
|
|
210
|
-
migrated:
|
|
211
|
-
subjects: [...new Set(
|
|
322
|
+
migrated: migrated.length,
|
|
323
|
+
subjects: [...new Set(migrated.map((r) => r.subject))].sort(),
|
|
212
324
|
};
|
|
213
325
|
});
|
|
214
326
|
}
|
|
@@ -52,12 +52,12 @@ declare const IntentRecordSchema: z.ZodObject<{
|
|
|
52
52
|
flow: z.ZodOptional<z.ZodString>;
|
|
53
53
|
files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
54
54
|
}, "strip", z.ZodTypeAny, {
|
|
55
|
-
route?: string | undefined;
|
|
56
55
|
flow?: string | undefined;
|
|
56
|
+
route?: string | undefined;
|
|
57
57
|
files?: string[] | undefined;
|
|
58
58
|
}, {
|
|
59
|
-
route?: string | undefined;
|
|
60
59
|
flow?: string | undefined;
|
|
60
|
+
route?: string | undefined;
|
|
61
61
|
files?: string[] | undefined;
|
|
62
62
|
}>>;
|
|
63
63
|
provenBy: z.ZodOptional<z.ZodObject<{
|
|
@@ -101,8 +101,8 @@ declare const IntentRecordSchema: z.ZodObject<{
|
|
|
101
101
|
statement: string;
|
|
102
102
|
declaredAt: number;
|
|
103
103
|
surface?: {
|
|
104
|
-
route?: string | undefined;
|
|
105
104
|
flow?: string | undefined;
|
|
105
|
+
route?: string | undefined;
|
|
106
106
|
files?: string[] | undefined;
|
|
107
107
|
} | undefined;
|
|
108
108
|
source?: string | undefined;
|
|
@@ -126,8 +126,8 @@ declare const IntentRecordSchema: z.ZodObject<{
|
|
|
126
126
|
statement: string;
|
|
127
127
|
declaredAt: number;
|
|
128
128
|
surface?: {
|
|
129
|
-
route?: string | undefined;
|
|
130
129
|
flow?: string | undefined;
|
|
130
|
+
route?: string | undefined;
|
|
131
131
|
files?: string[] | undefined;
|
|
132
132
|
} | undefined;
|
|
133
133
|
source?: string | undefined;
|
|
@@ -159,12 +159,12 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
159
159
|
flow: z.ZodOptional<z.ZodString>;
|
|
160
160
|
files: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
161
161
|
}, "strip", z.ZodTypeAny, {
|
|
162
|
-
route?: string | undefined;
|
|
163
162
|
flow?: string | undefined;
|
|
163
|
+
route?: string | undefined;
|
|
164
164
|
files?: string[] | undefined;
|
|
165
165
|
}, {
|
|
166
|
-
route?: string | undefined;
|
|
167
166
|
flow?: string | undefined;
|
|
167
|
+
route?: string | undefined;
|
|
168
168
|
files?: string[] | undefined;
|
|
169
169
|
}>>;
|
|
170
170
|
provenBy: z.ZodOptional<z.ZodObject<{
|
|
@@ -208,8 +208,8 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
208
208
|
statement: string;
|
|
209
209
|
declaredAt: number;
|
|
210
210
|
surface?: {
|
|
211
|
-
route?: string | undefined;
|
|
212
211
|
flow?: string | undefined;
|
|
212
|
+
route?: string | undefined;
|
|
213
213
|
files?: string[] | undefined;
|
|
214
214
|
} | undefined;
|
|
215
215
|
source?: string | undefined;
|
|
@@ -233,8 +233,8 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
233
233
|
statement: string;
|
|
234
234
|
declaredAt: number;
|
|
235
235
|
surface?: {
|
|
236
|
-
route?: string | undefined;
|
|
237
236
|
flow?: string | undefined;
|
|
237
|
+
route?: string | undefined;
|
|
238
238
|
files?: string[] | undefined;
|
|
239
239
|
} | undefined;
|
|
240
240
|
source?: string | undefined;
|
|
@@ -262,8 +262,8 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
262
262
|
statement: string;
|
|
263
263
|
declaredAt: number;
|
|
264
264
|
surface?: {
|
|
265
|
-
route?: string | undefined;
|
|
266
265
|
flow?: string | undefined;
|
|
266
|
+
route?: string | undefined;
|
|
267
267
|
files?: string[] | undefined;
|
|
268
268
|
} | undefined;
|
|
269
269
|
source?: string | undefined;
|
|
@@ -291,8 +291,8 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
291
291
|
statement: string;
|
|
292
292
|
declaredAt: number;
|
|
293
293
|
surface?: {
|
|
294
|
-
route?: string | undefined;
|
|
295
294
|
flow?: string | undefined;
|
|
295
|
+
route?: string | undefined;
|
|
296
296
|
files?: string[] | undefined;
|
|
297
297
|
} | undefined;
|
|
298
298
|
source?: string | undefined;
|
|
@@ -311,7 +311,6 @@ export declare const IntentShardSchema: z.ZodObject<{
|
|
|
311
311
|
}>;
|
|
312
312
|
}>;
|
|
313
313
|
export type IntentShard = z.infer<typeof IntentShardSchema>;
|
|
314
|
-
export declare const summarise: (statement: string) => string;
|
|
315
314
|
/** One line per intent: enough to decide whether to open the shard, and no more. */
|
|
316
315
|
declare const IntentIndexEntrySchema: z.ZodObject<{
|
|
317
316
|
id: z.ZodString;
|
|
@@ -74,7 +74,7 @@ export const IntentShardSchema = z.object({
|
|
|
74
74
|
* can do to a file meant to be read every session, which is the reason to keep it.
|
|
75
75
|
*/
|
|
76
76
|
const SUMMARY_MAX = 72;
|
|
77
|
-
|
|
77
|
+
const summarise = (statement) => statement.length <= SUMMARY_MAX ? statement : `${statement.slice(0, SUMMARY_MAX - 1).trimEnd()}…`;
|
|
78
78
|
/** One line per intent: enough to decide whether to open the shard, and no more. */
|
|
79
79
|
const IntentIndexEntrySchema = z.object({
|
|
80
80
|
id: z.string().min(1),
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type Intent, type IntentSurface } from '@reticlehq/core/artifacts';
|
|
2
2
|
import type { FileSystemPort } from '../project/fs/fs-port.js';
|
|
3
3
|
import type { Clock } from '../../machine/clock.js';
|
|
4
|
+
/**
|
|
5
|
+
* The intent ledger's domain operations: declare, place, bind, discharge.
|
|
6
|
+
*
|
|
7
|
+
* Every operation is a PURE transition from core (`redeclareIntent`, `bindIntent`,
|
|
8
|
+
* `dischargeIntent`) handed to the ledger's atomic upsert. Storage is `IntentShardStore` —
|
|
9
|
+
* `.reticle/intent/<subject>/intent.json`, a flow's name being its subject — and this class writes no
|
|
10
|
+
* file of its own, so there is exactly one place an intent can be.
|
|
11
|
+
*/
|
|
4
12
|
export declare class IntentStore {
|
|
5
13
|
#private;
|
|
6
14
|
constructor(fs: FileSystemPort, root: string, clock: Clock);
|
|
@@ -12,7 +20,8 @@ export declare class IntentStore {
|
|
|
12
20
|
* Declare one or more intents.
|
|
13
21
|
*
|
|
14
22
|
* Batched because the marginal cost of the whole mechanism has to stay at one call per feature; an
|
|
15
|
-
* agent that must make five calls to declare five things will make none.
|
|
23
|
+
* agent that must make five calls to declare five things will make none. Saying an intent again
|
|
24
|
+
* does not unsay it: see `redeclareIntent` for what survives.
|
|
16
25
|
*/
|
|
17
26
|
declare(entries: readonly {
|
|
18
27
|
id: string;
|
|
@@ -22,13 +31,9 @@ export declare class IntentStore {
|
|
|
22
31
|
/**
|
|
23
32
|
* File a record under where it turned out to be about.
|
|
24
33
|
*
|
|
25
|
-
* Separate from `declare` because
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the intent is ABOUT only exists after the consequence lands.
|
|
29
|
-
*
|
|
30
|
-
* Write-once: an existing surface is never overwritten. A record placed by an agent that named its
|
|
31
|
-
* own subject must not be re-filed by a later run that happened to be somewhere else.
|
|
34
|
+
* Separate from `declare` because an inline intent is declared BEFORE the action, and the route
|
|
35
|
+
* that describes it only exists after the consequence lands. Write-once: an existing surface is
|
|
36
|
+
* never overwritten. A flow named here moves the record into that flow's directory.
|
|
32
37
|
*/
|
|
33
38
|
place(id: string, surface: IntentSurface): Promise<boolean>;
|
|
34
39
|
/** Attach the predicate that would prove an intent. False when the id names nothing. */
|