leerness 1.36.185 → 1.36.187
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/CHANGELOG.md +17 -0
- package/README.ko.md +5 -0
- package/README.md +14 -4
- package/bin/leerness.js +291 -57
- package/docs/e2e-observation-contract.md +51 -0
- package/docs/runtime-compatibility-api.md +183 -0
- package/docs/state-paths-api.md +92 -0
- package/docs/state-scopes.md +266 -0
- package/docs/worktree-runtime-migration.md +278 -0
- package/lib/claims-baseline.js +1 -1
- package/lib/file-leases.js +2 -2
- package/lib/git.js +64 -15
- package/lib/io.js +9 -3
- package/lib/migrate.js +1 -1
- package/lib/preview-serve.js +5 -1
- package/lib/pure-utils.js +20 -0
- package/lib/role-fallback.js +3 -3
- package/lib/role-store.js +2 -2
- package/lib/runtime-layout.js +395 -0
- package/lib/runtime-writes.js +166 -0
- package/lib/session-close.js +1 -1
- package/lib/state-git.js +138 -0
- package/lib/state-inspect.js +39 -0
- package/lib/state-inventory.js +69 -0
- package/lib/state-paths.js +59 -0
- package/lib/workspace-dir.js +34 -10
- package/package.json +10 -6
- package/scripts/command-flags-probe.js +1 -1
- package/scripts/dead-flags-probe.js +1 -1
- package/scripts/e2e-child-diagnostics-probe.js +143 -0
- package/scripts/e2e-child-diagnostics.js +33 -0
- package/scripts/e2e-temp-scope-probe.js +127 -0
- package/scripts/e2e.js +108 -27
- package/scripts/encoding-selftest-probe.js +222 -0
- package/scripts/false-claim-probe.js +34 -3
- package/scripts/file-lease-probe.js +5 -2
- package/scripts/legacy-runtime-negative-probe.js +72 -0
- package/scripts/mutation-integrity-probe.js +3 -0
- package/scripts/role-fallback-probe.js +67 -6
- package/scripts/runtime-admission-probe.js +282 -0
- package/scripts/runtime-git-write-probe.js +116 -0
- package/scripts/runtime-layout-probe.js +1198 -0
- package/scripts/runtime-repl-probe.js +271 -0
- package/scripts/runtime-replacement-probe.js +190 -0
- package/scripts/runtime-write-probe.js +458 -0
- package/scripts/selftest-cleanup-probe.js +168 -0
- package/scripts/state-inspect-cli-probe.js +209 -0
- package/scripts/state-scopes-probe.js +590 -0
- package/scripts/workspace-dir-lock-order-probe.js +310 -35
- package/scripts/workspace-dir-migration-probe.js +17 -2
- package/scripts/workspace-selection-probe.js +230 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Worktree runtime migration: compatibility before relocation
|
|
2
|
+
|
|
3
|
+
Status: **P-0021 approved; T-0180 compatibility implementation under verification** (2026-09-06).
|
|
4
|
+
T-0179 records the original source audit. T-0175 / M-0015 remain incomplete.
|
|
5
|
+
Only A is authorized: diagnosis and observed-layout rejection. Storage relocation,
|
|
6
|
+
descriptor creation, activation, common control, finalize and cleanup are not implemented.
|
|
7
|
+
|
|
8
|
+
Source baseline: v1.36.186, commit `1805da9d2611a852be6a1b8636efa2977dd081f1`
|
|
9
|
+
(product source identical to release tag v1.36.186). Source references below use function
|
|
10
|
+
names so that line-number drift does not silently change the claimed boundary.
|
|
11
|
+
The [five-scope design](state-scopes.md) and [implemented inspection API](state-paths-api.md)
|
|
12
|
+
remain authoritative for the existing resolver. B/C below remain unimplemented proposals.
|
|
13
|
+
The [compatibility API](runtime-compatibility-api.md) describes the implemented A boundary.
|
|
14
|
+
|
|
15
|
+
## 1. Why a compatibility release comes first
|
|
16
|
+
|
|
17
|
+
`lib/state-paths.js:resolveStatePaths` currently returns `activeLayout: legacy`,
|
|
18
|
+
`runtimeActivated: false`, `migrationAvailable: false` as constants. Existing writers
|
|
19
|
+
continue to use `.leerness`. A new manifest is invisible to v1.36.186 and older clients;
|
|
20
|
+
removing `state.json` lets `_loadLeernessState` create a new legacy state instead of stopping them.
|
|
21
|
+
|
|
22
|
+
`bin/leerness.js:main` performs automatic workspace migration and usage recording before
|
|
23
|
+
ordinary handlers. `_recordRun` writes with `fs.appendFileSync`; execution-ledger code
|
|
24
|
+
also writes through file descriptors. Guarding only a handler or `writeUtf8` misses writes.
|
|
25
|
+
`_handoffVersionSkew` warns about older CLIs; it is not a write barrier. Installed Git
|
|
26
|
+
hooks read legacy freshness paths directly. These are source-audit findings, not executed
|
|
27
|
+
mixed-version or interruption tests.
|
|
28
|
+
|
|
29
|
+
The release sequence is therefore:
|
|
30
|
+
|
|
31
|
+
1. **A — compatibility only (P-0021 / T-0180):** strict layout reader, read-only diagnosis
|
|
32
|
+
and observed-layout write rejection. Legacy storage stays authoritative; no activation API.
|
|
33
|
+
2. **B — explicit migration (separate preview/approval):** prove participating clients are
|
|
34
|
+
upgraded and stopped, specify a writer-admission protocol, snapshot/copy/validate, then
|
|
35
|
+
activate one worktree and a named migration unit. Unknown participants mean defer activation.
|
|
36
|
+
3. **C — later units and integration:** migrate other coupled stores; connect M-0016 control,
|
|
37
|
+
M-0017 immutable records and M-0018 views. No claim that A completes these milestones.
|
|
38
|
+
|
|
39
|
+
A does not retroactively fence old or already-admitted writers. Even a check immediately
|
|
40
|
+
before a write has a check/write race; activation must not run concurrently with A-only
|
|
41
|
+
clients. Presence counts, elapsed time, PID probes and an empty lock directory do not prove
|
|
42
|
+
quiescence. The operator's maintenance declaration is a prerequisite, not an unbypassable
|
|
43
|
+
technical guarantee. Unmanaged clients, other clones and other hosts remain outside it.
|
|
44
|
+
|
|
45
|
+
## 2. Source-backed inventory and ownership
|
|
46
|
+
|
|
47
|
+
Paths in this table use the current canonical `.leerness/` spelling. Many writers hardcode
|
|
48
|
+
that directory; the configurable workspace selector does not redirect all of them. The 22
|
|
49
|
+
known surface names match `lib/state-inventory.js:SURFACES`; its current classification tags
|
|
50
|
+
are not field-level migration authority. This is **not a complete inventory of every cache or
|
|
51
|
+
mutating command**. The reader/writer examples identify coupling, not an exhaustive call graph.
|
|
52
|
+
Before B, each migration unit needs a closed writer/reader registry and tests for all entries.
|
|
53
|
+
|
|
54
|
+
| Surface(s) | Current readers / writers | Field ownership and disposition |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `cache/sessions/` | CLI `_sessionPresenceRecord`, `_readSessionEntries`, `_sessionSignal`, `sessionsCmd`; handoff, session close, hook session-start; `lib/session-presence.js` identity helpers | Per-session observations plus `.host-salt`. Preserve original address, source, timestamps and close tombstones. Not a live-agent registry. Smallest candidate unit: this entire directory and its consumers. |
|
|
57
|
+
| `cache/handoffs/` | `_recordHandoffFreshness`, `_latestHandoffFreshness`, `_getLastHandoffGap`; `enforceCmd` generated pre-commit and self-probe | Per-session freshness plus the reserved `unaddressed` marker. The hook uses filesystem mtime; copying must not manufacture a recent handoff. CLI and installed hooks must switch together. |
|
|
58
|
+
| `cache/agent-sessions/` | REPL `sessionPath` / `saveSession` | Raw conversation history, currently timestamp-addressed. Private; never automatically promote messages, prompts or history to tracked records. Owner mapping needs an explicit contract. |
|
|
59
|
+
| `cache/agent-runs/` | `_runsDir`, `_recordRun`, run-history consumers | Mixed/private intent and observations: one-shot records include raw user `task` text (up to 200 characters), provider/model, error and timing. Preserve task text and original IDs privately; not immutable reviewed completion evidence or cleanup-eligible disposable cache. |
|
|
60
|
+
| `cache/file-leases.json` | `lib/file-leases.js`, CLI `fileLeaseCmd` | Session owner, physical target identity, lease ID and expiry for this worktree. Keep distinct from repository task claims; do not move live leases into common control or silently renew TTL. |
|
|
61
|
+
| `state.json`, `runs/`, `handoff/` | `_loadLeernessState`, `_saveLeernessState`, `_loadRun`, `_saveRun`, `_updateRun`, `stateCmd` start/record/verify/handoff branches; MCP child calls | Coupled counter/current-run/session-owner map, per-run evidence and handoff artifact. Move only as one validated unit. Completed runs remain mutable today; M-0017 promotion is separate. |
|
|
62
|
+
| `execution-ledger.jsonl` | `lib/role-fallback.js`, agent history/record consumers | Mixed runtime provenance and potential durable-result inputs. Bind actual provider/model/fallback and reviewed revision when promoting later; the existing ignored ledger is not a finalize receipt. |
|
|
63
|
+
| `decisions.json`, `decisions.md` | `_loadDecisions`, `_saveDecisions` | Valid JSON array is the current authority; Markdown is a view/fallback. Strict import must reject invalid/non-array JSON rather than trust that fallback. Defer immutable import to M-0017. |
|
|
64
|
+
| `lessons.json`, `lessons.md` | `_loadLessons`, `_saveLessons` | Same strict-source boundary; validated lessons become separate immutable records only in M-0017. Preserve original Markdown text. |
|
|
65
|
+
| `progress-tracker.md` | Task parser/writers; `lib/session-close.js` | Task IDs, request, evidence, status and next action include project authority/user intent. Not disposable runtime. Keep unchanged until M-0018 has an accepted-input model. |
|
|
66
|
+
| `current-state.md` | Handoff readers; `lib/session-close.js:_upsertAutoLines` | User-written context plus generated auto lines. Preserve non-auto text; do not call the whole file a generated view. |
|
|
67
|
+
| `session-handoff.md` | Handoff readers; `lib/session-close.js` | Project instructions/context plus generated task summaries and session material. Split by ownership with original-byte retention, not by filename alone. |
|
|
68
|
+
| `active-wakeups.json` | `_loadActiveWakeups`, `_writeActiveWakeups`, `_recordWakeup`, `_analyzeWakeupStatus` | Requested schedule/source and runtime registration/status timestamps are mixed. Preserve scheduling intent; no scheduler changes or automatic execution in this migration. |
|
|
69
|
+
| `auto-resume-plan.json` | `_loadAutoResumePlan`, `_buildAutoResumePlan`, `_writeAutoResumePlan`, `resumeCmd` | `focus`, next-action titles/commands and note carry intent; `savedAt`, expected-fire data and context snapshot describe execution. Current task selection is not session ownership. Defer splitting. |
|
|
70
|
+
| `next-action-queue.json` | `_loadNextActionQueue`, `_writeNextActionQueue`, `_compactNextActionQueue` | Proposed titles/commands and IDs may encode intent. Preserve order, provenance and raw input; migration must not execute, silently deduplicate or rewrite these commands. |
|
|
71
|
+
| `pre-wake-report.json` | `_runPreWakeAudit`, pre-wake reporting, session-close integration | Mixed user-request copy and observations: `findings.critical[].items[].text` retains request text (up to 80 characters) beside IDs/counts/timestamps. Preserve the copy and its canonical request link; not proof of scheduled execution, liveness or safe disposal. |
|
|
72
|
+
| `last-handoff.json` | `_lastHandoffPath`, `_getLastHandoffGap`, legacy hook fallback | Legacy compatibility stamp, not a per-session ownership authority. Do not borrow another session's freshness. Include fallback retirement in the freshness unit. |
|
|
73
|
+
| `routing-log.json` | `lib/routing.js:routeCmd`, `_loadLog`, `_appendLog`, `_showLog`; dashboard reader | Mixed user intent and approval provenance: redacted original `task`, `declaredTier`, `riskDowngrade`, `requested` and `approvedBy` alongside assignments/observations. Bounded mutable list (200 records), not immutable approval evidence. Preserve fields and invalid-store rejection; no automatic cleanup or public promotion. |
|
|
74
|
+
|
|
75
|
+
Additional mutation paths outside the 22-surface metadata inventory include usage/update
|
|
76
|
+
caches, installation/migration metadata, hook installation, locks and session-close generated
|
|
77
|
+
artifacts. A's write-guard coverage must include these before any compatibility claim. Merely
|
|
78
|
+
adding 22 path checks is insufficient. Existing `check`/`audit`/`health` use
|
|
79
|
+
`lib/state-integrity.js:findCorruptedStateJson`, which only parses immediate workspace JSON,
|
|
80
|
+
skips some unreadable/empty cases and does not validate nested runtime layouts or schemas.
|
|
81
|
+
|
|
82
|
+
### Identity and minimum migration units
|
|
83
|
+
|
|
84
|
+
- Reuse `deriveSessionKey` and `normalizePresenceEnv`; keep case folding, explicit MCP
|
|
85
|
+
addresses and reserved-key rules. Do not derive ownership from PID, model, branch or newest run.
|
|
86
|
+
- Presence without a valid address remains unregistered. Existing anonymous structured-state
|
|
87
|
+
behavior is compatibility data, not permission to attribute it to the currently calling agent.
|
|
88
|
+
Import unknown ownership as unresolved; concurrent mutation needs an explicitly accepted owner.
|
|
89
|
+
- Preserve presence `.host-salt` privately with the directory; no public salt or identity export.
|
|
90
|
+
Preserve ambiguous case variants and invalid close records; do not prune during migration.
|
|
91
|
+
- Unit B1 candidate is presence alone. It still requires A and the B admission/maintenance
|
|
92
|
+
contract; it cannot be sold as full session/runtime isolation. Freshness with installed hooks
|
|
93
|
+
is B2; `state.json + runs + handoff` is B3. Unit selection is part of B's separate approval.
|
|
94
|
+
- A completed run's status does not make its bytes immutable. Existing verify-claim and
|
|
95
|
+
handoff evidence must keep resolving throughout B3. Do not reassign run counters or owner maps.
|
|
96
|
+
- `_leernessStateDir` also serves project `policy.json` via `_policyFile` / `_savePolicy`.
|
|
97
|
+
Replacing that helper wholesale would relocate policy with runtime; split the callers first.
|
|
98
|
+
- `state verify` records a supplied result; it does not execute tests. `session close` updates
|
|
99
|
+
tracked summaries and presence, but does not call `state handoff` or finalize immutable runs.
|
|
100
|
+
`runs list/show` instead reads `cache/agent-runs`, not the structured `runs/` directory.
|
|
101
|
+
- MCP `callLeerness` supplies a validated per-call session address when present; otherwise
|
|
102
|
+
inherited environment identity or anonymous state can still be shared. A stdio connection
|
|
103
|
+
ID is not a replacement ownership key. Invalid explicit CLI addresses may fall back to an
|
|
104
|
+
ambient address today; do not silently carry that behavior into a strict migration owner map.
|
|
105
|
+
- Agent-run task text, pre-wake request copies and routing approval provenance require a
|
|
106
|
+
retained private copy outside any checkout/private Git directory scheduled for removal, or
|
|
107
|
+
separately approved durable promotion that preserves their meaning and privacy. An existing
|
|
108
|
+
request/task link must be verified, not assumed to contain the whole original. Unresolved
|
|
109
|
+
retention blocks cleanup; M-0017 must not auto-publish raw task/approver/conversation data.
|
|
110
|
+
The old inspector's `runtime` tags do not authorize movement, deletion or lossy partitioning.
|
|
111
|
+
|
|
112
|
+
## 3. Approved compatibility slice (P-0021)
|
|
113
|
+
|
|
114
|
+
### Descriptor location and conservative decoding
|
|
115
|
+
|
|
116
|
+
Git compatibility descriptor: `<GIT_DIR>/leerness/layout.json`. This is a fixed,
|
|
117
|
+
worktree-wide admission location, deliberately **not keyed by projectKey**. An incompatible
|
|
118
|
+
descriptor conservatively blocks A's Leerness metadata writes for every subproject in that
|
|
119
|
+
worktree; it does not select a project's runtime. The runtime payload paths stay under
|
|
120
|
+
`<GIT_DIR>/leerness/projects/<projectKey>/runtime/` as proposed by the existing resolver.
|
|
121
|
+
|
|
122
|
+
`projectKey` is a path-derived namespace, not a persistent logical project identity. A branch
|
|
123
|
+
that renames a monorepo folder gets a new key but consults the same compatibility descriptor.
|
|
124
|
+
The descriptor is private metadata, not tracked knowledge or `.leerness/manifest.json`.
|
|
125
|
+
B still needs stable project binding or an explicitly approved relocation registry before
|
|
126
|
+
activating movable subprojects; unresolved moves must not create a new legacy authority.
|
|
127
|
+
Main-worktree Git directories can equal the common Git directory; the private `layout.json`
|
|
128
|
+
and `projects/…` remain distinct from `control/…`. Git documents linked worktrees' private
|
|
129
|
+
and common metadata separately. [Git worktree details](https://git-scm.com/docs/git-worktree#_details)
|
|
130
|
+
|
|
131
|
+
For genuinely non-Git projects the descriptor is the selection-independent
|
|
132
|
+
`<projectRoot>/.leerness/cache/state-layout.json`, the canonical sibling of the resolver's
|
|
133
|
+
`<projectRoot>/.leerness/cache/state-runtime/`. Do not root it in the selected `.harness`.
|
|
134
|
+
Changing `LEERNESS_WORKSPACE_DIR` must not choose another guard for the same writer target.
|
|
135
|
+
An alternate `.harness/cache/state-layout.json` or `.harness/cache/state-runtime` is an
|
|
136
|
+
ambiguous layout and blocks writes, even if a canonical descriptor also exists. No dual
|
|
137
|
+
authority or preference-based fallback. Workspace migration checks this before copying;
|
|
138
|
+
it must preserve or refuse, not relocate the descriptor as an ordinary cache file.
|
|
139
|
+
There is no common coordination store for non-Git. Introducing Git must also inspect the
|
|
140
|
+
canonical non-Git descriptor/runtime indicators and refuse an unresolved backend transition.
|
|
141
|
+
Git errors, inaccessible parents and damaged gitfiles must not choose a non-Git backend.
|
|
142
|
+
Ordinary non-Git use without a Git executable must remain supported: extract/reuse the
|
|
143
|
+
existing bounded repository-marker discovery semantics rather than catching all Git errors
|
|
144
|
+
as non-Git. Git-backed writes fail closed if their layout cannot be resolved. This discovery
|
|
145
|
+
change requires its own baseline regression tests and must not alter `state inspect` silently.
|
|
146
|
+
Caller workspace/Git-discovery overrides must not select a different admission authority for
|
|
147
|
+
identical canonical write targets. Unknown or conflicting topology/ownership blocks writes;
|
|
148
|
+
do not read foreign workspace content or fall back around a workspace conflict.
|
|
149
|
+
|
|
150
|
+
Descriptor fields: `schema: leerness.runtime-layout/v1`, `schemaVersion: 1`,
|
|
151
|
+
`scope: worktree` (Git) or `project-local` (non-Git), positive safe-integer `generation`, `layout: legacy`, and
|
|
152
|
+
`requiredWriterProtocol: 1`. A supports only the legacy layout/protocol combination.
|
|
153
|
+
Generation is provenance, **not an implemented fencing token**. A does not create descriptors;
|
|
154
|
+
it accepts externally supplied fixtures during tests and diagnoses future transition states.
|
|
155
|
+
No preview/apply/activate/rollback command is delivered in A.
|
|
156
|
+
|
|
157
|
+
| Observed descriptor/runtime state | A behavior |
|
|
158
|
+
|---|---|
|
|
159
|
+
| Descriptor absent and no new/alternate layout indicators | Legacy backend, no files created by diagnosis. |
|
|
160
|
+
| Strictly valid supported legacy descriptor; no new/alternate layout indicators | Keep legacy writes and location. Revalidate at mutation admission. |
|
|
161
|
+
| Git `leerness/projects/` exists (even empty), but fixed descriptor is absent or legacy | Ambiguous new/old-key layout: block the whole worktree without a recursive scan. A new projectKey must not bypass this check. |
|
|
162
|
+
| Non-Git proposed runtime, alternate descriptor/runtime or unresolved backend transition exists | Block writes; never pick whichever descriptor agrees with the current selection. |
|
|
163
|
+
| Invalid, empty, truncated, oversized, unreadable, linked or scope-mismatched descriptor | Structured bounded error; preserve bytes, no repair/fallback. |
|
|
164
|
+
| Future schema/protocol/layout | Incompatible: block writes before side effects; diagnostic reports the incompatibility. |
|
|
165
|
+
|
|
166
|
+
Reject unsafe descriptor parent links, use bounded regular-file reads (maximum
|
|
167
|
+
16 KiB), validate exact fields/types, and return reason codes rather than raw descriptor text.
|
|
168
|
+
Do not copy secrets, arbitrary paths or supplied commands into errors. A deleted descriptor
|
|
169
|
+
and deleted runtime cannot be recovered by inference; arbitrary external deletion is not fenced.
|
|
170
|
+
|
|
171
|
+
### Entry points and honest limits
|
|
172
|
+
|
|
173
|
+
Read-only CLI: `leerness state compatibility [path] --json`.
|
|
174
|
+
It reports compatibility separately from the existing metadata-only `state inspect` contract.
|
|
175
|
+
Diagnostic fields should include schema, observed layout, supported protocol, reason code,
|
|
176
|
+
write disposition and `activationSupported: false`; no mutation/counter/cache/salt/lock writes.
|
|
177
|
+
Missing data is not presented as a completed migration. No raw conversation is returned.
|
|
178
|
+
|
|
179
|
+
Use one domain-specific reader/guard (`lib/runtime-layout.js`), not a speculative
|
|
180
|
+
all-purpose StateManager. CLI and MCP execution must share it. Admission checks go before
|
|
181
|
+
auto workspace migration, usage, stale-update caching, lock creation and hook installation,
|
|
182
|
+
as well as at actual module/REPL/FD/log writer boundaries. Resolve the actual target project,
|
|
183
|
+
not the unrelated current directory. Global-only commands must not mutate or block that cwd.
|
|
184
|
+
Do not let existing `catch {}` best-effort logging turn an incompatible-layout rejection into
|
|
185
|
+
success. Recheck after waiting for a data lock and at each later mutation of a long-lived process.
|
|
186
|
+
|
|
187
|
+
This is **observed-layout rejection**, not atomic check/write fencing. A in-flight operation
|
|
188
|
+
can pass its check before an external descriptor changes. B must specify writer admission
|
|
189
|
+
and stop/drain all older participating clients before activation; it cannot infer safety from
|
|
190
|
+
A being installed somewhere. Initialization/update/downgrade/hook refresh must preserve or
|
|
191
|
+
refuse an incompatible descriptor, never reset it to legacy. A does not promise to modify
|
|
192
|
+
an already-installed old executable, external editor, Git hook or arbitrary worktree removal.
|
|
193
|
+
|
|
194
|
+
### Reuse and optimization constraints
|
|
195
|
+
|
|
196
|
+
- Reuse `lib/git.js` and the canonical project/worktree resolver. Do not shell-parse gitfiles
|
|
197
|
+
or add an independent Git subprocess wrapper. Scope/path discovery can share one immutable
|
|
198
|
+
invocation snapshot, but mutable compatibility decisions cannot be cached across writes,
|
|
199
|
+
lock waits, MCP requests or REPL turns.
|
|
200
|
+
- `writeUtf8` skips identical bytes and uses temp/rename. It is not create-only publication,
|
|
201
|
+
portable revision CAS, a multi-file transaction or a proven power-loss durability protocol.
|
|
202
|
+
- `writeBufferIfUnchanged` is Windows-only metadata-preserving replacement; POSIX currently
|
|
203
|
+
throws `E_METADATA_PRESERVATION_UNAVAILABLE`. Do not reuse it as a portable ControlStore CAS.
|
|
204
|
+
- `_withLock` serializes cooperating writers to one canonical target. Keep the existing
|
|
205
|
+
no-automatic-takeover policy for owner-bearing locks. It does not freeze another path or client.
|
|
206
|
+
- Existing `.harness` migration's inventory/hash/recheck/stage patterns are references only;
|
|
207
|
+
its lock is not taken by all writers, and its backup restore is not post-activation rollback.
|
|
208
|
+
- Node >=18, zero runtime dependencies, no network/provider call from diagnosis or admission.
|
|
209
|
+
Measure real-entry-point filesystem/Git calls and byte/mtime invariance before claiming an
|
|
210
|
+
optimization. Do not trade a fresh safety check for a faster stale cache.
|
|
211
|
+
|
|
212
|
+
## 4. Future B activation and recovery gates (not P-0021 implementation)
|
|
213
|
+
|
|
214
|
+
1. Enumerate the chosen unit's writers/readers, client versions, installed hooks, tracked
|
|
215
|
+
files and owners. Stop/upgrade participants and require an explicit maintenance declaration;
|
|
216
|
+
unknown old clients or unverifiable scope block activation. Recheck actual versions at use.
|
|
217
|
+
Establish the fixed compatibility boundary before any project-keyed staging/publication;
|
|
218
|
+
establish stable project bindings or refuse project moves. A metadata path hash is not a
|
|
219
|
+
logical identity. Do not activate B solely because A's observed-layout checks passed.
|
|
220
|
+
2. Read strict original bytes/schema, source file identity, timestamps, permissions and Git
|
|
221
|
+
tracking. Record a digest-bound preview and unresolved owners. Preserve invalid data;
|
|
222
|
+
do not use permissive loaders, repair, prune or merge-by-text as an import shortcut.
|
|
223
|
+
3. Stage copies in an exclusive private transaction directory; validate counts, IDs, counters,
|
|
224
|
+
ownership, provenance, content digests and required metadata. Source changes after preview
|
|
225
|
+
invalidate it. ACL/ADS/hard-link or cross-filesystem preservation unsupported by a backend
|
|
226
|
+
must cause refusal, not an unnoticed metadata loss. No runtime content enters a tracked archive.
|
|
227
|
+
4. Under the future admission protocol, revalidate inputs and switch one versioned authority
|
|
228
|
+
descriptor. No dual writable legacy/new truth. Crash injection is required before/after each
|
|
229
|
+
publication point; uncertain commit means recovery-required, not automatic success.
|
|
230
|
+
5. Before activation, rollback discards only owned staging after identity validation and leaves
|
|
231
|
+
originals untouched. After activation/new writes, first quiesce and reconcile newer events;
|
|
232
|
+
never copy an old backup over them. Preserve an explicit recovery receipt and artifacts.
|
|
233
|
+
6. Separately preview any Git index/ignore change and retain original files. Preserve user text
|
|
234
|
+
in mixed documents; no blanket untrack/delete or merge=union. Worktree removal remains gated
|
|
235
|
+
on M-0017 durable finalization and M-0018 adapter support, not merely a successful copy.
|
|
236
|
+
|
|
237
|
+
Filesystem rename alone is not evidence of durable multi-file commit. Node's fsync API
|
|
238
|
+
documents OS/device-dependent flush behavior; a future backend must state and test its actual
|
|
239
|
+
durability support instead of promising recovery from every power failure.
|
|
240
|
+
[Node 18 fsync documentation](https://nodejs.org/docs/latest-v18.x/api/fs.html#fsfsyncsyncfd)
|
|
241
|
+
|
|
242
|
+
## 5. Acceptance and delivery evidence
|
|
243
|
+
|
|
244
|
+
P-0021 approval authorizes A only. Implementation acceptance must include:
|
|
245
|
+
|
|
246
|
+
- A legacy control with no descriptor has unchanged command output/bytes apart from its
|
|
247
|
+
already documented writes. Main/two linked worktrees, sibling monorepo projects, detached
|
|
248
|
+
HEAD, aliases and non-Git remain distinct as specified by the resolver.
|
|
249
|
+
- Missing, valid legacy, malformed, empty, oversized, permission-denied, symlink/junction,
|
|
250
|
+
unknown protocol and deleted-descriptor-with-runtime cases give deterministic dispositions.
|
|
251
|
+
- A cross-branch monorepo subproject rename keeps the fixed worktree guard; old-key runtime
|
|
252
|
+
with a missing descriptor blocks even under a new key. Different genuine worktrees stay
|
|
253
|
+
isolated. The coarse whole-worktree block is explicit in diagnostics, not labelled per-project.
|
|
254
|
+
- Non-Git `.harness`/`.leerness` forced-selection variants consult the same canonical guard;
|
|
255
|
+
alternate descriptors/dual runtimes, foreign paths, Git discovery overrides and introducing
|
|
256
|
+
Git cannot reopen legacy writes against an incompatible or unresolved target.
|
|
257
|
+
- Fixtures with agent-run task text, pre-wake copied request text and routing approver/risk
|
|
258
|
+
provenance remain private and byte-preserved. A retains the current inspector tags as
|
|
259
|
+
metadata-only hints; future migration needs strict field classification and retention proof.
|
|
260
|
+
- A pre-existing incompatible descriptor produces zero writes through actual CLI startup,
|
|
261
|
+
MCP calls, REPL, direct module invocation, FD append, usage/update cache, locks, init/update
|
|
262
|
+
and hook-install paths. Unsupported writer paths block the compatibility release, not only B.
|
|
263
|
+
- Descriptor changes while a command waits on a lock are re-read. A changes-after-final-check
|
|
264
|
+
race is documented and not passed off as fencing. No activation under A-only writers.
|
|
265
|
+
- Old v1.36.186 and an already-running legacy writer are negative controls: the tests must
|
|
266
|
+
demonstrate they do not honor the new guard. This is a known limit, not a suppressed failure.
|
|
267
|
+
- `state inspect` retains metadata-only/no-content/no-write behavior. The new diagnostic is
|
|
268
|
+
independently tested for bounded content, errors, no outputs of secrets and byte/mtime stability.
|
|
269
|
+
- Existing presence, handoff, structured-state, MCP, lease and fallback probes; installed
|
|
270
|
+
cleanroom; supported Node and Windows/POSIX CI; independent Codex review; then public release
|
|
271
|
+
verification. No test count or release claim from v1.36.186 is reused as evidence for A.
|
|
272
|
+
|
|
273
|
+
The original T-0179 planning round ran document/reference checks and source review only.
|
|
274
|
+
The approved T-0180 round adds implementation and regression evidence, including real old
|
|
275
|
+
v1.36.186 CLI and already-loaded writer negative controls. Those clients ignore the descriptor;
|
|
276
|
+
this is an observed limit, not evidence that activation is safe. Final cross-platform/full
|
|
277
|
+
release validation is tracked separately in review-evidence.md; it is not implied by this design.
|
|
278
|
+
The broader UR-0097 and migration B/C remain open.
|
package/lib/claims-baseline.js
CHANGED
package/lib/file-leases.js
CHANGED
|
@@ -838,8 +838,8 @@ module.exports = {
|
|
|
838
838
|
normalizeTtlSeconds,
|
|
839
839
|
resolveTarget,
|
|
840
840
|
readStore,
|
|
841
|
-
acquire,
|
|
842
|
-
release,
|
|
841
|
+
acquire: require('./runtime-writes').projectWriter(acquire),
|
|
842
|
+
release: require('./runtime-writes').projectWriter(release),
|
|
843
843
|
list,
|
|
844
844
|
check,
|
|
845
845
|
};
|
package/lib/git.js
CHANGED
|
@@ -50,31 +50,80 @@ const _shouldDrop = (name) => { const u = String(name).toUpperCase(); return _DR
|
|
|
50
50
|
const _GIT_MUTATING = new Set(['add', 'rm', 'mv', 'commit', 'merge', 'rebase', 'reset', 'checkout', 'switch',
|
|
51
51
|
'restore', 'branch', 'tag', 'push', 'fetch', 'pull', 'clone', 'init', 'worktree', 'stash', 'apply', 'cherry-pick',
|
|
52
52
|
'revert', 'clean', 'gc', 'prune', 'update-ref', 'symbolic-ref', 'notes', 'submodule', 'config', 'remote']);
|
|
53
|
-
//
|
|
53
|
+
// Parse only explicit read options. Unknown/negated action flags remain writes;
|
|
54
|
+
// option values and tokens after -- must never masquerade as a read selector.
|
|
55
|
+
function _gitReadArgs(rest, switches, values = new Set(), optional = new Set()) {
|
|
56
|
+
const operands = [], flags = new Set();
|
|
57
|
+
for (let i = 0; i < rest.length; i++) {
|
|
58
|
+
const token = rest[i];
|
|
59
|
+
if (token === '--') { operands.push(...rest.slice(i + 1)); break; }
|
|
60
|
+
if (!token.startsWith('-')) { operands.push(token); continue; }
|
|
61
|
+
const name = token.split('=', 1)[0];
|
|
62
|
+
if (values.has(name)) {
|
|
63
|
+
if (!token.includes('=')) {
|
|
64
|
+
if (rest[i + 1] !== undefined && (!optional.has(name) || !rest[i + 1].startsWith('-'))) i++;
|
|
65
|
+
else if (!optional.has(name)) return null;
|
|
66
|
+
}
|
|
67
|
+
} else if (!switches.test(token)) return null;
|
|
68
|
+
flags.add(name);
|
|
69
|
+
}
|
|
70
|
+
return { operands, flags };
|
|
71
|
+
}
|
|
72
|
+
function _gitRefReadForm(sub, rest) {
|
|
73
|
+
const filters = new Set(['--contains', '--no-contains', '--merged', '--no-merged', '--points-at']);
|
|
74
|
+
const values = new Set([...filters, '--sort', '--format']);
|
|
75
|
+
const optional = new Set([...filters].filter(value => sub === 'tag' || value !== '--points-at'));
|
|
76
|
+
const common = /^(?:--list|--(?:no-)?(?:color|column)(?:=.*)?|--(?:no-)?(?:ignore-case|omit-empty)|-[li])$/;
|
|
77
|
+
const extra = sub === 'branch'
|
|
78
|
+
? /^(?:-[alrvi]+|--(?:all|remotes|show-current|verbose|no-verbose|no-abbrev)|--abbrev(?:=\d+)?)$/
|
|
79
|
+
: /^(?:-[lvi]+|-n\d*|--verify)$/;
|
|
80
|
+
const parsed = _gitReadArgs(rest, { test: token => common.test(token) || extra.test(token) }, values, optional);
|
|
81
|
+
if (!parsed) return false;
|
|
82
|
+
const listing = [...parsed.flags].some(flag => filters.has(flag) || flag === '--list'
|
|
83
|
+
|| (sub === 'branch' ? /^(?:--all|--remotes|--show-current|-[alrvi]*[alr][alrvi]*)$/ : /^(?:--verify|-n\d*|-[lvi]*[lv][lvi]*)$/).test(flag));
|
|
84
|
+
return listing || parsed.operands.length === 0;
|
|
85
|
+
}
|
|
86
|
+
// 조회성 하위명령은 이름이 같아도 플래그/인자 수로 갈라진다.
|
|
54
87
|
const _GIT_READ_ONLY_FORM = (sub, rest) => {
|
|
55
|
-
if (sub === 'branch') return
|
|
56
|
-
if (sub === 'config')
|
|
57
|
-
|
|
88
|
+
if (sub === 'branch' || sub === 'tag') return _gitRefReadForm(sub, rest);
|
|
89
|
+
if (sub === 'config') {
|
|
90
|
+
const parsed = _gitReadArgs(rest,
|
|
91
|
+
/^(?:--(?:get|get-all|get-regexp|get-urlmatch|get-color|get-colorbool|list|local|global|system|worktree|null|name-only|show-origin|show-scope|includes|no-includes|fixed-value|bool|int|bool-or-int|path|expiry-date)|-[lz])$/,
|
|
92
|
+
new Set(['--file', '-f', '--blob', '--type', '--default']));
|
|
93
|
+
return !!parsed && [...parsed.flags].some(flag => /^(?:--get(?:-all|-regexp|-urlmatch|-color|-colorbool)?|--list|-l)$/.test(flag));
|
|
94
|
+
}
|
|
95
|
+
if (sub === 'symbolic-ref') {
|
|
96
|
+
const parsed = _gitReadArgs(rest, /^(?:-q|--(?:no-)?(?:quiet|short|recurse))$/);
|
|
97
|
+
return !!parsed && parsed.operands.length === 1;
|
|
98
|
+
}
|
|
58
99
|
if (sub === 'worktree') return rest[0] === 'list';
|
|
59
100
|
if (sub === 'stash') return rest[0] === 'list' || rest[0] === 'show';
|
|
60
|
-
if (sub === 'tag') return !rest.some((x) => /^-(d|a|s|f)$|^--(delete|annotate|sign|force)/.test(x));
|
|
61
101
|
if (sub === 'notes') return rest[0] === 'list' || rest[0] === 'show';
|
|
62
102
|
if (sub === 'submodule') return rest[0] === 'status' || rest[0] === 'summary';
|
|
63
|
-
if (sub === 'remote')
|
|
103
|
+
if (sub === 'remote') {
|
|
104
|
+
let i = 0;
|
|
105
|
+
while (rest[i] === '-v' || rest[i] === '--verbose') i++;
|
|
106
|
+
return i === rest.length || rest[i] === 'show' || rest[i] === 'get-url';
|
|
107
|
+
}
|
|
64
108
|
return false;
|
|
65
109
|
};
|
|
66
110
|
let _gitDry = false;
|
|
67
111
|
function setGitDryRun(on) { _gitDry = !!on; }
|
|
68
112
|
function gitSpawn(args, opts) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
// Git도 index/hook 설정 등 메타데이터를 쓸 수 있다. 관측 경계 안에서 실행되는
|
|
114
|
+
// 변경 명령은 fs writer와 동일하게 다시 확인한다 (topology 조회는 제외).
|
|
115
|
+
const candidate = Array.isArray(args) ? args.map(String) : [];
|
|
116
|
+
let cursor = 0;
|
|
117
|
+
while (cursor < candidate.length && candidate[cursor].startsWith('-')) {
|
|
118
|
+
cursor += ['-C', '-c', '--git-dir', '--work-tree', '--namespace'].includes(candidate[cursor]) ? 2 : 1;
|
|
119
|
+
}
|
|
120
|
+
const verb = candidate[cursor] || '';
|
|
121
|
+
const mutating = _GIT_MUTATING.has(verb) && !_GIT_READ_ONLY_FORM(verb, candidate.slice(cursor + 1));
|
|
122
|
+
if (mutating) {
|
|
123
|
+
require('./runtime-writes').assertCurrentRuntimeWrite();
|
|
124
|
+
}
|
|
125
|
+
if (_gitDry && mutating) {
|
|
126
|
+
throw Object.assign(new Error(`--dry-run 인데 git ${verb} 를 실행하려 했습니다`), { code: 'E_DRY_RUN_WRITE', file: `git ${verb}` });
|
|
78
127
|
}
|
|
79
128
|
const _argv = ['--no-optional-locks', ...args]; // 사용자 저장소의 `.git/index` 재기록을 막는다(실측)
|
|
80
129
|
// ⚠ 키를 **접어서** 비교한다 — Windows 환경변수는 대소문자를 구분하지 않는데 JS 객체 키는 구분해서,
|
package/lib/io.js
CHANGED
|
@@ -179,7 +179,7 @@ function setDryRunGuard(on) { _dryGuard = !!on; if (on) _patchFsForDryRun(); }
|
|
|
179
179
|
// File-descriptor based writers cannot be intercepted by the fs path mutator
|
|
180
180
|
// wrappers. Call this before opening a descriptor so --dry-run retains its
|
|
181
181
|
// process-wide zero-persistent-write contract.
|
|
182
|
-
function assertWriteAllowed(p) { _dryCheck(p); }
|
|
182
|
+
function assertWriteAllowed(p) { _dryCheck(p); require('./runtime-writes').assertCurrentRuntimeWrite(p); }
|
|
183
183
|
// ⚠ `mkdirp` 은 가드하지 **않는다**. 한 번 넣었다가 부작용을 재서 뺀다 —
|
|
184
184
|
// `_withLock` 이 락 디렉토리를 만드는 것까지 막혀 락 획득이 실패하고 **무보호 진행**으로 떨어졌다
|
|
185
185
|
// (실측: `state start --dry-run` → "락 획득 실패 — 보호 없이 진행합니다"). 빈 디렉토리는 사용자 데이터가
|
|
@@ -355,10 +355,11 @@ function _replaceWindowsWithBackup(from, to, backup) {
|
|
|
355
355
|
let attempts = 0;
|
|
356
356
|
for (;;) {
|
|
357
357
|
attempts++;
|
|
358
|
+
assertWriteAllowed(to); // spawn-based replacement is outside fs interception; retries need fresh admission too.
|
|
358
359
|
const r = cp.spawnSync(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command], {
|
|
359
360
|
encoding: 'utf8',
|
|
360
361
|
windowsHide: true,
|
|
361
|
-
timeout:
|
|
362
|
+
timeout: 30000,
|
|
362
363
|
env: {
|
|
363
364
|
...process.env,
|
|
364
365
|
LEERNESS_REPLACE_FROM: from,
|
|
@@ -635,4 +636,9 @@ function detachCommittedHardLink(aliasPath, livePath, expectedBytes) {
|
|
|
635
636
|
};
|
|
636
637
|
}
|
|
637
638
|
|
|
638
|
-
|
|
639
|
+
const { pathWriter } = require('./runtime-writes');
|
|
640
|
+
module.exports = { log, ok, warn, fail, failJson, setQuiet, today, now, absRoot, exists, read, readBuf,
|
|
641
|
+
mkdirp: pathWriter(mkdirp), writeUtf8: pathWriter(writeUtf8),
|
|
642
|
+
writeBufferIfUnchanged: pathWriter(writeBufferIfUnchanged), append: pathWriter(append),
|
|
643
|
+
rel, setDryRunGuard, assertWriteAllowed: pathWriter(assertWriteAllowed),
|
|
644
|
+
mkdirpRaw: pathWriter(mkdirpRaw), detachCommittedHardLink: pathWriter(detachCommittedHardLink) };
|
package/lib/migrate.js
CHANGED
|
@@ -142,4 +142,4 @@ function migratePlanCmd(root, opts = {}, deps = {}) {
|
|
|
142
142
|
log(`\n 전체 적용: leerness update --yes --path ${root} · canonical만: leerness migrate apply --path ${root} --yes`);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
module.exports = { migrateAuditCmd, migrateApplyCmd, migratePlanCmd };
|
|
145
|
+
module.exports = { migrateAuditCmd, migrateApplyCmd: require('./runtime-writes').projectWriter(migrateApplyCmd), migratePlanCmd };
|
package/lib/preview-serve.js
CHANGED
|
@@ -278,4 +278,8 @@ function previewModeCmd(root, val, deps = {}) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// _listenWithFallback 공개(1.36.90): 대시보드가 **같은 런타임**을 쓴다 — 두 번째 서버 구현을 만들지 않는다.
|
|
281
|
-
|
|
281
|
+
const { projectWriter } = require('./runtime-writes');
|
|
282
|
+
module.exports = { LEERNESS_PREVIEW_PORT, previewServeCmd: projectWriter(previewServeCmd),
|
|
283
|
+
previewModeCmd, loadPreviewConfig, savePreviewConfig: projectWriter(savePreviewConfig),
|
|
284
|
+
cleanupServeDir: projectWriter(cleanupServeDir), serveDir, modeQuestionLines,
|
|
285
|
+
buildServeWorkspace: projectWriter(buildServeWorkspace), _listenWithFallback };
|
package/lib/pure-utils.js
CHANGED
|
@@ -275,6 +275,23 @@ function _parseSlashFromHelp(text, invoke = 'slash') {
|
|
|
275
275
|
// 1.9.283 (UR-0025 2단계): 권한 등급(permission tiers) 순수 로직 — capabilities/policy 공유.
|
|
276
276
|
const PERMISSION_TIERS = ['read-only', 'safe-write', 'project-write', 'shell-read', 'shell-write', 'git-write', 'network', 'publish'];
|
|
277
277
|
function _tierRank(t) { const i = PERMISSION_TIERS.indexOf(String(t || '')); return i < 0 ? PERMISSION_TIERS.length : i; }
|
|
278
|
+
// A narrow literal-text check for the inspection exemption, not a shell parser.
|
|
279
|
+
// Quoted path punctuation is data; operators outside quotes and expansion syntax
|
|
280
|
+
// retain the ordinary conservative classification below.
|
|
281
|
+
function _isLiteralInspectionText(text) {
|
|
282
|
+
let quote = null;
|
|
283
|
+
for (let i = 0; i < text.length; i++) {
|
|
284
|
+
const ch = text[i], next = text[i + 1] || '';
|
|
285
|
+
if (/[\r\n]/.test(ch)) return false;
|
|
286
|
+
if (quote) {
|
|
287
|
+
if (ch === quote) quote = null;
|
|
288
|
+
else if (quote === '"' && (ch === '`' || (ch === '$' && /[a-z0-9_{(]/i.test(next))
|
|
289
|
+
|| (ch === '\\' && next === '"'))) return false;
|
|
290
|
+
} else if (ch === '"' || ch === "'") quote = ch;
|
|
291
|
+
else if (/[;&|<>()$`^]/.test(ch) || (ch === '\\' && /["']/.test(next))) return false;
|
|
292
|
+
}
|
|
293
|
+
return quote === null && !/%[^%\r\n]+%/.test(text);
|
|
294
|
+
}
|
|
278
295
|
// 명령/capability → 요구 등급 (순수 매핑)
|
|
279
296
|
function _requiredTier(cmd) {
|
|
280
297
|
const c = String(cmd || '').toLowerCase();
|
|
@@ -284,6 +301,9 @@ function _requiredTier(cmd) {
|
|
|
284
301
|
const leaseCommand = c.match(/^\s*(?:(?:npx(?:\s+(?:-y|--yes))?\s+)?leerness(?:@[^\s]+)?\s+)?lease\s+(list|check|acquire|release)(?:\s|$)/);
|
|
285
302
|
if (leaseCommand && (leaseCommand[1] === 'list' || leaseCommand[1] === 'check')) return 'read-only';
|
|
286
303
|
if (leaseCommand) return 'safe-write';
|
|
304
|
+
// This exemption describes one inspect invocation, never composite shell text.
|
|
305
|
+
if (/^\s*(?:(?:npx(?:\s+(?:-y|--yes))?\s+)?leerness(?:@[^\s]+)?\s+)?state\s+inspect(?:\s|$)/.test(c)
|
|
306
|
+
&& _isLiteralInspectionText(c)) return 'read-only';
|
|
287
307
|
if (/release\s+publish|npm\s+publish|\bpublish\b/.test(c)) return 'publish';
|
|
288
308
|
if (/\bweb\b/.test(c)) return 'network';
|
|
289
309
|
if (/git\s+push|sync-main/.test(c)) return 'git-write';
|
package/lib/role-fallback.js
CHANGED
|
@@ -1597,8 +1597,8 @@ module.exports = {
|
|
|
1597
1597
|
buildCandidatePool,
|
|
1598
1598
|
normalizeAvailability,
|
|
1599
1599
|
normalizeAvailabilityObservation,
|
|
1600
|
-
appendAvailabilityObservation,
|
|
1601
|
-
appendAvailabilityClear,
|
|
1600
|
+
appendAvailabilityObservation: require('./runtime-writes').projectWriter(appendAvailabilityObservation),
|
|
1601
|
+
appendAvailabilityClear: require('./runtime-writes').projectWriter(appendAvailabilityClear),
|
|
1602
1602
|
readAvailabilityObservations,
|
|
1603
1603
|
availabilityExtrasForCandidate,
|
|
1604
1604
|
sessionIdentityFromEnv,
|
|
@@ -1606,6 +1606,6 @@ module.exports = {
|
|
|
1606
1606
|
selectFallbackOption,
|
|
1607
1607
|
executionLedgerPath,
|
|
1608
1608
|
normalizeExecutionEvent,
|
|
1609
|
-
appendExecutionEvent,
|
|
1609
|
+
appendExecutionEvent: require('./runtime-writes').projectWriter(appendExecutionEvent),
|
|
1610
1610
|
readExecutionEvents,
|
|
1611
1611
|
};
|
package/lib/role-store.js
CHANGED
|
@@ -720,8 +720,8 @@ module.exports = {
|
|
|
720
720
|
revisionForBytes,
|
|
721
721
|
readRoleStore,
|
|
722
722
|
loadRoles,
|
|
723
|
-
saveRoles,
|
|
724
|
-
updateRoles,
|
|
723
|
+
saveRoles: require('./runtime-writes').projectWriter(saveRoles),
|
|
724
|
+
updateRoles: require('./runtime-writes').projectWriter(updateRoles),
|
|
725
725
|
setRoleAssignment,
|
|
726
726
|
resolveRole,
|
|
727
727
|
validationSummary,
|