@north-light/crouter 0.3.301 → 0.3.302
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/dist/api/dto/reports.d.ts +8 -8
- package/dist/clients/attach/viewer.js +394 -394
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +30 -1
- package/dist/commands/memory/read.js +12 -14
- package/dist/commands/node/lifecycle.js +7 -6
- package/dist/commands/push.js +4 -4
- package/dist/core/__tests__/integration/revive.test.js +3 -3
- package/dist/core/__tests__/seam/dormancy-release.test.js +42 -0
- package/dist/core/__tests__/stop-guard.test.js +64 -0
- package/dist/core/canvas/crons.d.ts +20 -4
- package/dist/core/canvas/crons.js +54 -7
- package/dist/core/memory-resolver.d.ts +3 -1
- package/dist/core/memory-resolver.js +3 -3
- package/dist/core/nested-stores.d.ts +1 -1
- package/dist/core/nested-stores.js +3 -3
- package/dist/core/runtime/bearings-render.js +1 -1
- package/dist/core/runtime/close.js +8 -6
- package/dist/core/runtime/stop-guard.js +6 -6
- package/dist/core/worktree.js +3 -6
- package/dist/daemon/api/__tests__/seam/leaf-api-parity.test.js +21 -11
- package/dist/daemon/api/handlers/reports.js +1 -1
- package/dist/daemon/api/map.d.ts +1 -1
- package/dist/daemon/api/map.js +2 -2
- package/dist/daemon/cron/sinks.d.ts +1 -1
- package/dist/daemon/cron/sinks.js +2 -2
- package/dist/daemon/reconcilers/live-obligation.js +2 -2
- package/package.json +1 -1
- package/runtime.lock.json +5 -5
- package/dist/daemon/cron-sink.d.ts +0 -26
- package/dist/daemon/cron-sink.js +0 -43
package/dist/core/worktree.js
CHANGED
|
@@ -954,12 +954,9 @@ export function autoDropCleanManagedWorktreeLocked(nodeId, wt, proof) {
|
|
|
954
954
|
* `open_managed_worktree` guard then still applies unchanged — real
|
|
955
955
|
* commits, real uncommitted changes, and a lost race all require the
|
|
956
956
|
* explicit path. */
|
|
957
|
-
function
|
|
957
|
+
function cleanCurrentCheckoutTip(wt) {
|
|
958
958
|
if (wt.state !== 'open' || !existsSync(wt.path) || isRebaseInProgress(wt.path))
|
|
959
959
|
return null;
|
|
960
|
-
const branch = gitSync(['rev-parse', '--abbrev-ref', 'HEAD'], wt.path);
|
|
961
|
-
if (branch.status !== 0 || branch.stdout.trim() !== wt.branch)
|
|
962
|
-
return null;
|
|
963
960
|
const status = gitSync(['status', '--porcelain'], wt.path);
|
|
964
961
|
if (status.status !== 0 || status.stdout.trim() !== '')
|
|
965
962
|
return null;
|
|
@@ -967,7 +964,7 @@ function cleanCurrentBranchTip(wt) {
|
|
|
967
964
|
return head.status === 0 ? head.stdout.trim() : null;
|
|
968
965
|
}
|
|
969
966
|
export async function branchIsPushedAtCurrentTip(wt) {
|
|
970
|
-
const initialTip =
|
|
967
|
+
const initialTip = cleanCurrentCheckoutTip(wt);
|
|
971
968
|
if (initialTip === null)
|
|
972
969
|
return false;
|
|
973
970
|
// A cached origin branch can be stale in either direction. Query origin's
|
|
@@ -989,7 +986,7 @@ export async function branchIsPushedAtCurrentTip(wt) {
|
|
|
989
986
|
return false;
|
|
990
987
|
// The remote proof is useful only if the local checkout is still the exact
|
|
991
988
|
// clean tip it inspected when finalization begins.
|
|
992
|
-
return
|
|
989
|
+
return cleanCurrentCheckoutTip(wt) === initialTip;
|
|
993
990
|
}
|
|
994
991
|
export function autoDropCleanManagedWorktree(nodeId) {
|
|
995
992
|
const wt = openManagedWorktreeForNode(nodeId);
|
|
@@ -29,7 +29,7 @@ import { CrtrClient } from '../../../../api/index.js';
|
|
|
29
29
|
import { registerPush } from '../../../../commands/push.js';
|
|
30
30
|
import { registerNode } from '../../../../commands/node.js';
|
|
31
31
|
import { createNode, getNode } from '../../../../core/canvas/canvas.js';
|
|
32
|
-
import { createManagedWorktree } from '../../../../core/worktree.js';
|
|
32
|
+
import { branchIsPushedAtCurrentTip, createManagedWorktree } from '../../../../core/worktree.js';
|
|
33
33
|
import { readInboxSince } from '../../../../core/feed/inbox.js';
|
|
34
34
|
import { gitSync } from '../../../../core/git.js';
|
|
35
35
|
import { InputError } from '../../../../core/io.js';
|
|
@@ -187,28 +187,38 @@ test('re-homed guard: `crtr push final` on a node with a managed worktree holdin
|
|
|
187
187
|
rmSync(repo, { recursive: true, force: true });
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
|
-
test('`crtr push final`
|
|
190
|
+
test('`crtr push final` retains a clean managed worktree checked out on a pushed PR branch (#1109, #1061)', async () => {
|
|
191
191
|
const repo = initRepo();
|
|
192
192
|
const remote = mkdtempSync(join(tmpdir(), 'crtr-leafparity-remote-'));
|
|
193
193
|
try {
|
|
194
194
|
git(['init', '--bare'], remote);
|
|
195
195
|
git(['remote', 'add', 'origin', remote], repo);
|
|
196
196
|
git(['push', '-u', 'origin', 'main'], repo);
|
|
197
|
-
const id = 'wt-
|
|
197
|
+
const id = 'wt-pushed-pr-branch-final';
|
|
198
198
|
const wt = createManagedWorktree(repo, id);
|
|
199
199
|
writeFileSync(join(wt.path, 'delivered-via-pr.txt'), 'delivered via a PR branch\n');
|
|
200
200
|
git(['add', '.'], wt.path);
|
|
201
|
-
git(['commit', '-m', 'delivered via
|
|
201
|
+
git(['commit', '-m', 'delivered via PR branch'], wt.path);
|
|
202
202
|
const tip = gitSync(['rev-parse', 'HEAD'], wt.path).stdout.trim();
|
|
203
|
-
const
|
|
204
|
-
git(['
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const prBranch = 'fix/pushed-pr-worktree-final';
|
|
204
|
+
git(['switch', '-c', prBranch], wt.path);
|
|
205
|
+
git(['push', 'origin', `HEAD:refs/heads/${wt.branch}`], wt.path);
|
|
206
|
+
git(['push', 'origin', `HEAD:refs/heads/${prBranch}`], wt.path);
|
|
207
|
+
assert.equal(gitSync(['rev-parse', `refs/heads/${wt.branch}`], wt.path).stdout.trim(), tip, 'the recorded branch remains at the checkout tip');
|
|
208
|
+
assert.equal(gitSync(['rev-parse', '--abbrev-ref', 'HEAD'], wt.path).stdout.trim(), prBranch, 'the checkout uses the PR branch instead of the recorded branch');
|
|
209
|
+
assert.equal(gitSync(['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${wt.branch}`], wt.path).stdout.split('\t')[0], tip, 'origin carries the recorded branch tip');
|
|
210
|
+
assert.equal(gitSync(['ls-remote', '--exit-code', '--heads', 'origin', `refs/heads/${prBranch}`], wt.path).stdout.split('\t')[0], tip, 'origin carries the PR branch tip');
|
|
211
|
+
const untracked = join(wt.path, 'generated.uid');
|
|
212
|
+
writeFileSync(untracked, 'untracked generated file\n');
|
|
213
|
+
assert.equal(await branchIsPushedAtCurrentTip(wt), false, 'an untracked file still rejects the origin-retention proof');
|
|
214
|
+
rmSync(untracked);
|
|
207
215
|
createNode(worktreeNode(id, wt.path, wt));
|
|
208
|
-
const out = await runPushAs(id, 'final', 'delivered through a
|
|
216
|
+
const out = await runPushAs(id, 'final', 'delivered through a PR branch');
|
|
209
217
|
assert.equal(out['worktree_auto_dropped'], undefined, 'the remote-tip allowance retains the open worktree');
|
|
210
|
-
assert.equal(getNode(id)?.managed_worktree?.state, 'open', 'the remote-tip allowance does not close the worktree');
|
|
211
|
-
assert.equal(getNode(id)?.
|
|
218
|
+
assert.equal(getNode(id)?.managed_worktree?.state, 'open', 'the remote-tip allowance does not close the worktree record');
|
|
219
|
+
assert.equal(getNode(id)?.managed_worktree?.branch, wt.branch, 'the retained record keeps its recorded branch');
|
|
220
|
+
assert.equal(gitSync(['rev-parse', '--abbrev-ref', 'HEAD'], wt.path).stdout.trim(), prBranch, 'finalization does not change the checked-out PR branch');
|
|
221
|
+
assert.equal(getNode(id)?.status, 'done', 'the node finalizes when both pushed branches have its exact tip');
|
|
212
222
|
}
|
|
213
223
|
finally {
|
|
214
224
|
rmSync(repo, { recursive: true, force: true });
|
|
@@ -100,7 +100,7 @@ async function guardFinalWorktree(id) {
|
|
|
100
100
|
// command-local mapper needed (plan C-5).
|
|
101
101
|
throw new InputError({
|
|
102
102
|
error: 'open_managed_worktree',
|
|
103
|
-
message: 'This node still owns an open managed worktree — `push final`
|
|
103
|
+
message: 'This node still owns an open managed worktree that is neither clean and locally landed nor clean with its exact tip on origin — `push final` cannot finish.',
|
|
104
104
|
next: (() => {
|
|
105
105
|
const wt = getNode(id)?.managed_worktree;
|
|
106
106
|
return wt !== null && wt !== undefined && wt.state === 'open' && !existsSync(wt.path)
|
package/dist/daemon/api/map.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ManagedWorktree, NodeMeta, NodeRow, NodeRuntime, SubscriptionRef } from '../../core/canvas/types.js';
|
|
2
2
|
import type { OutcomeDeliveryRecord } from '../../core/canvas/node-outcome-deliveries.js';
|
|
3
3
|
import type { ReviewComment, ReviewCommentEvent, ReviewErrorCode, ReviewRecord, ReviewResult } from '../../core/review/types.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type Cron, type CronRunRecord } from '../../core/canvas/crons.js';
|
|
5
5
|
import type { ProfileEntry } from '../../core/profiles/manifest.js';
|
|
6
6
|
import type { ReviveResult } from '../../core/runtime/revive.js';
|
|
7
7
|
import type { CloseNodeResult } from '../../core/runtime/close.js';
|
package/dist/daemon/api/map.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { existsSync, readFileSync } from 'node:fs';
|
|
13
13
|
import { outputResultPath } from '../../core/runtime/structured-output.js';
|
|
14
14
|
import { ReviewOperationError as ReviewOperationErrorClass } from '../../core/review/types.js';
|
|
15
|
+
import { cronSinkDisplay } from '../../core/canvas/crons.js';
|
|
15
16
|
import { isProfilePaused, sanitizeProfileMetadata } from '../../core/profiles/manifest.js';
|
|
16
17
|
import { contextDir, inboxPath, nodeDir, nodeMetaPath, reportsDir, reviewResultPath, transcriptPath, viewSocketPath, } from '../../core/canvas/paths.js';
|
|
17
18
|
import { FinalizationError } from '../../core/feed/feed.js';
|
|
@@ -21,7 +22,6 @@ import { CrtrError } from '../../core/errors.js';
|
|
|
21
22
|
import { WorktreeError } from '../../core/worktree.js';
|
|
22
23
|
import { ApiError } from '../../api/errors.js';
|
|
23
24
|
import { ExitCode } from '../../types.js';
|
|
24
|
-
import { sinkDisplay } from '../cron-sink.js';
|
|
25
25
|
import { responsePath } from '../../core/human/convention.js';
|
|
26
26
|
import { ticketDir } from '../../core/human/root.js';
|
|
27
27
|
// Node projections
|
|
@@ -314,7 +314,7 @@ export function toCronDTO(c, lastRun) {
|
|
|
314
314
|
run_timeout_s: c.run_timeout_s,
|
|
315
315
|
overlap: c.overlap,
|
|
316
316
|
on_output: c.on_output,
|
|
317
|
-
sink:
|
|
317
|
+
sink: cronSinkDisplay(c.sink),
|
|
318
318
|
tier: c.tier,
|
|
319
319
|
state: c.state,
|
|
320
320
|
held: c.held,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Cron } from '../../core/canvas/crons.js';
|
|
2
2
|
export declare function deliverToSink(c: Cron, stdout: string): Promise<string>;
|
|
@@ -13,7 +13,7 @@ import { reviveNode } from '../../core/runtime/revive.js';
|
|
|
13
13
|
import { isBrokerLive } from '../../core/runtime/model-swap.js';
|
|
14
14
|
import { spawnChild } from '../../core/runtime/spawn.js';
|
|
15
15
|
import { cronWakeOrigin } from '../../core/runtime/bearings.js';
|
|
16
|
-
import {
|
|
16
|
+
import { parseCronSink } from '../../core/canvas/crons.js';
|
|
17
17
|
import { resolveLiveProfile, resolveLiveProfileName } from './armed-context.js';
|
|
18
18
|
async function deliverNodeSink(c, target, stdout) {
|
|
19
19
|
const meta = getNode(target);
|
|
@@ -89,7 +89,7 @@ function deliverHumanSink(c, stdout) {
|
|
|
89
89
|
publishPage(ticketDir(id), prepared);
|
|
90
90
|
}
|
|
91
91
|
export async function deliverToSink(c, stdout) {
|
|
92
|
-
const sink =
|
|
92
|
+
const sink = parseCronSink(c.sink);
|
|
93
93
|
if (sink === null)
|
|
94
94
|
return 'no sink armed — nothing delivered';
|
|
95
95
|
if (sink.kind === 'node') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newestReportRef, hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber,
|
|
1
|
+
import { newestReportRef, hasActiveLiveSubscription, hasLiveMessageWait, hasLiveSubscriber, hasPendingCronWake, subscribersOf, } from '../../core/canvas/index.js';
|
|
2
2
|
import { fullName } from '../../core/canvas/labels.js';
|
|
3
3
|
import { countTickets } from '../../core/canvas/attention.js';
|
|
4
4
|
import { hasUnseenWakeMail, readCursor, readInboxSince } from '../../core/feed/inbox.js';
|
|
@@ -12,7 +12,7 @@ import { transition } from '../../core/runtime/lifecycle.js';
|
|
|
12
12
|
export function hasLiveObligation(nodeId) {
|
|
13
13
|
return hasActiveLiveSubscription(nodeId)
|
|
14
14
|
|| hasLiveMessageWait(nodeId)
|
|
15
|
-
||
|
|
15
|
+
|| hasPendingCronWake(nodeId)
|
|
16
16
|
|| countTickets(nodeId) > 0
|
|
17
17
|
|| readOutputRequest(nodeId).state !== 'absent';
|
|
18
18
|
}
|
package/package.json
CHANGED
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/crouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.302",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/crouter",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.302",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"workspaces": [
|
|
@@ -5206,17 +5206,17 @@
|
|
|
5206
5206
|
},
|
|
5207
5207
|
"packages/crouter-api": {
|
|
5208
5208
|
"name": "@north-light/crouter-api",
|
|
5209
|
-
"version": "0.3.
|
|
5209
|
+
"version": "0.3.302",
|
|
5210
5210
|
"license": "UNLICENSED"
|
|
5211
5211
|
},
|
|
5212
5212
|
"packages/crouter-env-docker": {
|
|
5213
5213
|
"name": "@north-light/crouter-env-docker",
|
|
5214
|
-
"version": "0.3.
|
|
5214
|
+
"version": "0.3.302",
|
|
5215
5215
|
"license": "UNLICENSED"
|
|
5216
5216
|
},
|
|
5217
5217
|
"packages/crouter-sdk": {
|
|
5218
5218
|
"name": "@north-light/crouter-sdk",
|
|
5219
|
-
"version": "0.3.
|
|
5219
|
+
"version": "0.3.302",
|
|
5220
5220
|
"license": "UNLICENSED",
|
|
5221
5221
|
"dependencies": {
|
|
5222
5222
|
"@north-light/crouter-api": "^0.3.295"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** The one decoder for a cron's stored sink JSON.
|
|
2
|
-
*
|
|
3
|
-
* A sink is armed as a CLI spec string (`node:<id>` | `spawn:<kind>` |
|
|
4
|
-
* `human`), validated and encoded to JSON once at arm time
|
|
5
|
-
* (`api/handlers/crons.ts`), and thereafter read back in exactly two places:
|
|
6
|
-
* the runner, which dispatches a delivery on it, and the DTO mapper, which
|
|
7
|
-
* renders it. Both go through here so the stored shape has a single reader.
|
|
8
|
-
*
|
|
9
|
-
* A malformed stored value decodes as no-sink rather than throwing: a cron
|
|
10
|
-
* that cannot deliver must still run, and must still list.
|
|
11
|
-
*/
|
|
12
|
-
export type ParsedSink = {
|
|
13
|
-
kind: 'node';
|
|
14
|
-
node: string;
|
|
15
|
-
} | {
|
|
16
|
-
kind: 'spawn';
|
|
17
|
-
spawn: string;
|
|
18
|
-
} | {
|
|
19
|
-
kind: 'human';
|
|
20
|
-
} | null;
|
|
21
|
-
/** Stored JSON → typed shape. `{}`, malformed JSON, and an unrecognised shape
|
|
22
|
-
* all read as null (no sink). */
|
|
23
|
-
export declare function parseSink(sinkJson: string): ParsedSink;
|
|
24
|
-
/** Stored JSON → the display spec the CLI armed with, or null when the cron
|
|
25
|
-
* never delivers. The exact inverse of the arm-time encoding. */
|
|
26
|
-
export declare function sinkDisplay(sinkJson: string): string | null;
|
package/dist/daemon/cron-sink.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/** The one decoder for a cron's stored sink JSON.
|
|
2
|
-
*
|
|
3
|
-
* A sink is armed as a CLI spec string (`node:<id>` | `spawn:<kind>` |
|
|
4
|
-
* `human`), validated and encoded to JSON once at arm time
|
|
5
|
-
* (`api/handlers/crons.ts`), and thereafter read back in exactly two places:
|
|
6
|
-
* the runner, which dispatches a delivery on it, and the DTO mapper, which
|
|
7
|
-
* renders it. Both go through here so the stored shape has a single reader.
|
|
8
|
-
*
|
|
9
|
-
* A malformed stored value decodes as no-sink rather than throwing: a cron
|
|
10
|
-
* that cannot deliver must still run, and must still list.
|
|
11
|
-
*/
|
|
12
|
-
/** Stored JSON → typed shape. `{}`, malformed JSON, and an unrecognised shape
|
|
13
|
-
* all read as null (no sink). */
|
|
14
|
-
export function parseSink(sinkJson) {
|
|
15
|
-
try {
|
|
16
|
-
const s = JSON.parse(sinkJson);
|
|
17
|
-
if (typeof s['node'] === 'string')
|
|
18
|
-
return { kind: 'node', node: s['node'] };
|
|
19
|
-
if (typeof s['spawn'] === 'string')
|
|
20
|
-
return { kind: 'spawn', spawn: s['spawn'] };
|
|
21
|
-
if (s['human'] === true)
|
|
22
|
-
return { kind: 'human' };
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
// malformed stored sink reads as no-sink; callers report it in their own terms
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
/** Stored JSON → the display spec the CLI armed with, or null when the cron
|
|
30
|
-
* never delivers. The exact inverse of the arm-time encoding. */
|
|
31
|
-
export function sinkDisplay(sinkJson) {
|
|
32
|
-
const sink = parseSink(sinkJson);
|
|
33
|
-
if (sink === null)
|
|
34
|
-
return null;
|
|
35
|
-
switch (sink.kind) {
|
|
36
|
-
case 'node':
|
|
37
|
-
return `node:${sink.node}`;
|
|
38
|
-
case 'spawn':
|
|
39
|
-
return `spawn:${sink.spawn}`;
|
|
40
|
-
case 'human':
|
|
41
|
-
return 'human';
|
|
42
|
-
}
|
|
43
|
-
}
|