@north-light/crouter 0.3.226 → 0.3.228
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/client.d.ts +5 -5
- package/dist/api/client.js +3 -3
- package/dist/api/dto/broker-ops.d.ts +8 -0
- package/dist/api/dto/memory.d.ts +8 -7
- package/dist/api/dto/memory.js +2 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
- package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
- package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
- package/dist/builtin-memory/design/guide.md +38 -16
- package/dist/builtin-memory/design/roadmap.md +4 -4
- package/dist/builtin-memory/insights/init.md +5 -5
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
- package/dist/builtin-memory/internal/memory-loading.md +6 -6
- package/dist/builtin-memory/internal/plugins.md +1 -1
- package/dist/builtin-memory/plan/guide.md +53 -0
- package/dist/builtin-memory/plan/roadmap.md +10 -8
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
- package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
- package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
- package/dist/clients/attach/chrome/inbox-strip.js +11 -7
- package/dist/clients/attach/session/pane-tag.d.ts +28 -2
- package/dist/clients/attach/session/pane-tag.js +48 -22
- package/dist/clients/attach/session/profile-files.d.ts +6 -0
- package/dist/clients/attach/session/profile-files.js +44 -11
- package/dist/clients/attach/slash/dispatch.js +4 -7
- package/dist/clients/attach/viewer.js +588 -596
- package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
- package/dist/commands/api-client.d.ts +3 -7
- package/dist/commands/api-client.js +7 -12
- package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
- package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
- package/dist/commands/memory/delete.js +51 -24
- package/dist/commands/memory/edit.js +50 -10
- package/dist/commands/memory/find.js +105 -56
- package/dist/commands/memory/history.js +51 -31
- package/dist/commands/memory/lint.d.ts +0 -9
- package/dist/commands/memory/lint.js +78 -322
- package/dist/commands/memory/list.d.ts +12 -4
- package/dist/commands/memory/list.js +64 -25
- package/dist/commands/memory/move.js +103 -73
- package/dist/commands/memory/origin.js +35 -5
- package/dist/commands/memory/read.d.ts +4 -0
- package/dist/commands/memory/read.js +132 -80
- package/dist/commands/memory/shared.d.ts +115 -25
- package/dist/commands/memory/shared.js +331 -74
- package/dist/commands/memory/write.js +72 -21
- package/dist/commands/memory.js +2 -2
- package/dist/commands/node/lifecycle.js +2 -2
- package/dist/commands/pkg/market-manage.js +25 -24
- package/dist/commands/pkg/plugin-manage.d.ts +22 -6
- package/dist/commands/pkg/plugin-manage.js +118 -25
- package/dist/commands/pkg/shared.d.ts +8 -0
- package/dist/commands/pkg/shared.js +23 -20
- package/dist/commands/surface-reload.d.ts +5 -0
- package/dist/commands/surface-reload.js +9 -1
- package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
- package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
- package/dist/commands/sys/migrate.js +126 -140
- package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
- package/dist/commands/sys/panels/profiles-panel.js +599 -0
- package/dist/commands/sys/settings-shell.d.ts +4 -2
- package/dist/commands/sys/settings-shell.js +35 -4
- package/dist/commands/sys/settings.js +21 -5
- package/dist/commands/sys/sync-deps.d.ts +2 -0
- package/dist/commands/sys/sync-deps.js +26 -15
- package/dist/commands/sys/sync-project-guidance.js +228 -145
- package/dist/commands/sys/sync-skills.js +28 -17
- package/dist/commands/sys/update.js +11 -3
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
- package/dist/core/__tests__/config-change-delta.test.js +106 -0
- package/dist/core/__tests__/context-intro.test.js +13 -9
- package/dist/core/__tests__/daemon-boot.test.js +22 -16
- package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
- package/dist/core/__tests__/human-deliver.test.js +19 -0
- package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
- package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
- package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
- package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
- package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
- package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
- package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
- package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
- package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
- package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
- package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
- package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
- package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
- package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
- package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
- package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
- package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
- package/dist/core/__tests__/kickoff.test.js +16 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
- package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
- package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
- package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
- package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
- package/dist/core/__tests__/repository-association.test.d.ts +1 -0
- package/dist/core/__tests__/repository-association.test.js +153 -0
- package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
- package/dist/core/__tests__/repository-root-identity.test.js +219 -0
- package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
- package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
- package/dist/core/canvas/canvas.d.ts +3 -0
- package/dist/core/canvas/canvas.js +30 -10
- package/dist/core/canvas/db.js +28 -1
- package/dist/core/canvas/paths.d.ts +4 -6
- package/dist/core/canvas/paths.js +8 -6
- package/dist/core/canvas/render-source.js +2 -2
- package/dist/core/canvas/types.d.ts +9 -13
- package/dist/core/exclusive-lock.d.ts +2 -0
- package/dist/core/exclusive-lock.js +21 -0
- package/dist/core/git.d.ts +0 -1
- package/dist/core/git.js +0 -3
- package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
- package/dist/core/human/feedback-companion.js +3 -0
- package/dist/core/human/scan.js +8 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +2 -1
- package/dist/core/memory/doc-link-grammar.js +2 -2
- package/dist/core/memory/history.d.ts +24 -0
- package/dist/core/memory/history.js +66 -1
- package/dist/core/memory/identity.d.ts +65 -0
- package/dist/core/memory/identity.js +185 -0
- package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
- package/dist/core/memory/inline-ref-guidance.js +1 -1
- package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
- package/dist/core/memory/inline-ref-inventory.js +30 -62
- package/dist/core/memory/lint.d.ts +129 -0
- package/dist/core/memory/lint.js +517 -0
- package/dist/core/memory/project-namespace.d.ts +31 -0
- package/dist/core/memory/project-namespace.js +85 -0
- package/dist/core/memory/repository-association.d.ts +31 -0
- package/dist/core/memory/repository-association.js +129 -0
- package/dist/core/memory/tree.d.ts +39 -0
- package/dist/core/memory/tree.js +93 -0
- package/dist/core/memory-resolver.d.ts +193 -90
- package/dist/core/memory-resolver.js +461 -375
- package/dist/core/nested-stores.js +6 -13
- package/dist/core/profiles/select.d.ts +4 -1
- package/dist/core/profiles/select.js +108 -48
- package/dist/core/review/__tests__/capture-origin.test.js +2 -2
- package/dist/core/review/__tests__/stage-identity.test.js +2 -2
- package/dist/core/review/companion.js +11 -2
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +3 -3
- package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
- package/dist/core/runtime/broker/rebind.js +5 -0
- package/dist/core/runtime/broker-extension-render.d.ts +7 -3
- package/dist/core/runtime/broker-extension-render.js +10 -5
- package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
- package/dist/core/runtime/broker-persona-guidance.js +122 -27
- package/dist/core/runtime/deliver-live.d.ts +16 -4
- package/dist/core/runtime/deliver-live.js +29 -15
- package/dist/core/runtime/kickoff.d.ts +3 -3
- package/dist/core/runtime/kickoff.js +8 -16
- package/dist/core/runtime/lifecycle.js +2 -3
- package/dist/core/runtime/nodes.d.ts +3 -4
- package/dist/core/runtime/nodes.js +3 -4
- package/dist/core/runtime/persona.d.ts +8 -12
- package/dist/core/runtime/persona.js +24 -96
- package/dist/core/runtime/promote.d.ts +2 -2
- package/dist/core/runtime/promote.js +8 -21
- package/dist/core/runtime/revive.js +20 -17
- package/dist/core/runtime/spawn.js +18 -7
- package/dist/core/runtime/tmux-bindings.js +2 -3
- package/dist/core/runtime/warm-pool.js +3 -4
- package/dist/core/scope.js +2 -0
- package/dist/core/self-update.d.ts +0 -2
- package/dist/core/self-update.js +2 -35
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
- package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
- package/dist/core/substrate/index.d.ts +2 -2
- package/dist/core/substrate/index.js +1 -1
- package/dist/core/substrate/injected-store.d.ts +43 -27
- package/dist/core/substrate/injected-store.js +208 -104
- package/dist/core/substrate/listings.d.ts +19 -12
- package/dist/core/substrate/listings.js +75 -52
- package/dist/core/substrate/on-read-node.d.ts +4 -7
- package/dist/core/substrate/on-read-node.js +6 -8
- package/dist/core/substrate/on-read.d.ts +22 -25
- package/dist/core/substrate/on-read.js +103 -147
- package/dist/core/substrate/render-node.d.ts +4 -7
- package/dist/core/substrate/render-node.js +5 -7
- package/dist/core/substrate/render.d.ts +21 -3
- package/dist/core/substrate/render.js +291 -223
- package/dist/core/substrate/schema.d.ts +1 -13
- package/dist/core/substrate/schema.js +5 -40
- package/dist/core/substrate/session-cache.d.ts +14 -4
- package/dist/core/substrate/session-cache.js +40 -22
- package/dist/core/substrate/surface-match.d.ts +9 -7
- package/dist/core/substrate/surface-match.js +26 -25
- package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
- package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
- package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
- package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +37 -9
- package/dist/daemon/api/bridge.d.ts +35 -4
- package/dist/daemon/api/bridge.js +50 -8
- package/dist/daemon/api/handlers/broker-ops.js +21 -16
- package/dist/daemon/api/handlers/memory.js +2 -0
- package/dist/daemon/crtrd.js +2 -0
- package/dist/daemon/human/finish.js +8 -1
- package/dist/daemon/manage.d.ts +0 -1
- package/dist/daemon/manage.js +7 -16
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/migrations/001-surfaces-frontmatter.js +21 -109
- package/dist/migrations/002-profile-project-memory.js +1 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
- package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
- package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
- package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
- package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
- package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
- package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
- package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
- package/dist/migrations/__tests__/activation.test.d.ts +1 -0
- package/dist/migrations/__tests__/activation.test.js +149 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
- package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
- package/dist/migrations/activation.d.ts +16 -0
- package/dist/migrations/activation.js +78 -0
- package/dist/migrations/convergent.d.ts +14 -3
- package/dist/migrations/convergent.js +21 -10
- package/dist/migrations/corpus.d.ts +78 -0
- package/dist/migrations/corpus.js +497 -0
- package/dist/migrations/frontmatter-splice.d.ts +15 -0
- package/dist/migrations/frontmatter-splice.js +176 -0
- package/dist/migrations/registry.d.ts +6 -1
- package/dist/migrations/registry.js +7 -2
- package/dist/migrations/runner.d.ts +41 -0
- package/dist/migrations/runner.js +81 -0
- package/dist/migrations/types.d.ts +148 -9
- package/dist/migrations/types.js +9 -2
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
- package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
- package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
- package/dist/pi-extensions/canvas-context-intro.js +50 -46
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
- package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
- package/dist/pi-extensions/canvas-stophook.js +6 -13
- package/dist/shared/generated-context.d.ts +0 -3
- package/dist/shared/generated-context.js +0 -57
- package/dist/shared/tool-groups.js +2 -3
- package/package.json +5 -4
- package/runtime.lock.json +2 -2
- /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
- /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
- /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
- /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
- /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
- /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
- /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
- /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
- /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
- /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
- /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
- /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
kind: knowledge
|
|
3
|
-
when-and-why-to-read: When choosing between a flat plan and a decomposed one, or synthesizing part-plans into an index, this knowledge should be read so a planning effort splits only where
|
|
3
|
+
when-and-why-to-read: When choosing between a flat plan and a decomposed one, or synthesizing part-plans into an index, this knowledge should be read so a planning effort splits only where independent execution repays the synthesis it costs.
|
|
4
4
|
short-form: Use when deciding whether a planning effort splits into part-plans, and how to synthesize them into one index.
|
|
5
5
|
gate: {kind: plan}
|
|
6
6
|
rationale: >-
|
|
7
|
-
Carries plan shape and decomposition only. The
|
|
7
|
+
Carries plan shape and decomposition only. The implementation-unit contract lives in [[plan/guide]] so every planner can use it whether or not the effort ever needs a roadmap; do not pull generic planning guidance back in here.
|
|
8
8
|
surfaces:
|
|
9
9
|
- on: boot
|
|
10
10
|
at: preview
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
# Plan
|
|
13
|
+
# Plan shapes and the decomposition decision
|
|
14
14
|
|
|
15
|
-
Every planning effort produces either
|
|
15
|
+
Every planning effort produces either one flat plan or an index with part-plans. Choose decomposition for worthwhile parallel planning, not raw size: a flat plan can span many yields, while part-plans add delegation and synthesis cost that independent slices must repay.
|
|
16
16
|
|
|
17
17
|
## Choosing a shape
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Use a **flat plan** when one coherent owner can map the work at consistent unit granularity. It follows [[plan/guide]] in one file.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Use a **decomposed plan** when settled dependency edges expose slices that can be planned independently, their edit ownership does not overlap, and the effort is large enough that parallel work materially improves intelligence, productivity, or elapsed time. A frontend/backend distinction alone is not a decomposition trigger when both sides still share an unsettled seam or edit surface.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Produce a navigable index and delegate each slice to a `plan`-kind child with the approved spec and design, explicit scope, dependencies, and ownership boundary. A slice goes to a `plan` sub-orchestrator only when it independently passes the same parallelism threshold; a long sequential slice stays with a base child across yields.
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Each part-plan follows [[plan/guide]]. The index links every part-plan and carries a lane table with each plan, its outcome, dependencies, ownership boundary, and proof gate. It also maps every acceptance criterion to the responsible part-plan or a final cross-lane runtime gate. Unit detail stays in the part-plans.
|
|
26
|
+
|
|
27
|
+
After the part-plans land, synthesize the index before declaring done. Resolve file and symbol ownership conflicts, align names and phase gates, fill integration gaps, confirm the dependency graph permits every claimed parallel lane, and prove that part-plan and cross-lane gates cover the full approved outcome. Do not duplicate part-plan units in the index.
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { listMemoryDocs } from "../../../commands/memory/list.js";
|
|
3
2
|
import { createMemoryDocSnapshot, readMemoryDocContent } from "../../../commands/memory/read.js";
|
|
3
|
+
import { parseSubstrateDoc } from "../../../core/substrate/schema.js";
|
|
4
4
|
import {
|
|
5
5
|
expandDeterministicCommand,
|
|
6
6
|
type DeterministicCommandExpansion,
|
|
7
7
|
} from "../../../core/runtime/command-expansion.js";
|
|
8
8
|
import { expandShellBlocks, hasShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
|
|
9
9
|
import type { ChatCommandMetadata } from "../../../core/runtime/command-surface.js";
|
|
10
|
+
import {
|
|
11
|
+
exposureTarget,
|
|
12
|
+
loadContextExposureState,
|
|
13
|
+
mergeContextExposureState,
|
|
14
|
+
registerDocumentExposure,
|
|
15
|
+
saveContextExposureState,
|
|
16
|
+
sharedContextExposureState,
|
|
17
|
+
} from "../../../core/substrate/injected-store.js";
|
|
10
18
|
import { piShellRunner } from "./pi-shell-runner.js";
|
|
11
19
|
|
|
12
20
|
// ---------------------------------------------------------------------------
|
|
@@ -16,57 +24,50 @@ import { piShellRunner } from "./pi-shell-runner.js";
|
|
|
16
24
|
// the doc body (plus the user's free-text args) as a normal turn instead of
|
|
17
25
|
// requiring a separate skill-invocation mechanism.
|
|
18
26
|
//
|
|
19
|
-
// Discovery uses
|
|
20
|
-
// snapshot
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// scope-precedence sorted (see `scopeRank` in crouter's memory/shared.ts), so
|
|
24
|
-
// first-occurrence-wins per doc name reproduces the resolver's identity
|
|
25
|
-
// precedence for free. The inventory carries parsed `slash` flags and resolved
|
|
26
|
-
// paths for command bodies.
|
|
27
|
+
// Discovery uses one in-process target snapshot for both listing and body
|
|
28
|
+
// resolution. The snapshot already resolves exact canonical identities in
|
|
29
|
+
// source precedence; this file does not walk memory directories or reimplement
|
|
30
|
+
// scope resolution.
|
|
27
31
|
// ---------------------------------------------------------------------------
|
|
28
32
|
|
|
29
33
|
interface MemoryListItem {
|
|
30
34
|
name: string;
|
|
31
|
-
path: string;
|
|
32
35
|
short_form: string;
|
|
33
36
|
slash: boolean;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export interface SlashDoc {
|
|
37
|
-
/**
|
|
38
|
-
* `crtr memory read` resolves. */
|
|
40
|
+
/** Canonical name resolved by `crtr memory read`. */
|
|
39
41
|
name: string;
|
|
40
|
-
/**
|
|
41
|
-
* unchanged). */
|
|
42
|
+
/** Canonical `/` segments rendered as ordinary `:` command segments. */
|
|
42
43
|
commandName: string;
|
|
43
44
|
/** The memory document's short-form description, shown in the slash menu. */
|
|
44
45
|
description: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
48
|
+
// Keep each canonical winner's body and path for disclosure classification and
|
|
49
|
+
// the existing execution read.
|
|
49
50
|
const resolvedMemoryDocs = new Map<string, { path: string; canonicalBody: string }>();
|
|
50
51
|
|
|
51
52
|
async function defaultListMemoryDocs(): Promise<MemoryListItem[]> {
|
|
52
53
|
const snapshot = createMemoryDocSnapshot();
|
|
53
|
-
const items =
|
|
54
|
-
name: doc.name,
|
|
55
|
-
path: doc.path,
|
|
56
|
-
short_form: doc.shortForm,
|
|
57
|
-
slash: doc.slash,
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
// Slash eligibility follows the list identity, while the body follows the
|
|
61
|
-
// separate `memory read <name>` resolver contract (including direct-path
|
|
62
|
-
// precedence). Resolve all eligible names against one source snapshot.
|
|
54
|
+
const items: MemoryListItem[] = [];
|
|
63
55
|
const seen = new Set<string>();
|
|
64
|
-
const
|
|
65
|
-
if (seen.has(
|
|
66
|
-
seen.add(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
for (const memoryDoc of snapshot.docs) {
|
|
57
|
+
if (seen.has(memoryDoc.name)) continue;
|
|
58
|
+
seen.add(memoryDoc.name);
|
|
59
|
+
const doc = parseSubstrateDoc(memoryDoc);
|
|
60
|
+
if (doc === null) continue;
|
|
61
|
+
items.push({
|
|
62
|
+
name: memoryDoc.name,
|
|
63
|
+
short_form: doc.shortForm,
|
|
64
|
+
slash: doc.slash,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve eligible canonical names against the same target snapshot that
|
|
69
|
+
// supplied the list rows.
|
|
70
|
+
const resolved = snapshot.resolve(items.filter((item) => item.slash).map((item) => item.name));
|
|
70
71
|
resolvedMemoryDocs.clear();
|
|
71
72
|
for (const [name, doc] of resolved) {
|
|
72
73
|
resolvedMemoryDocs.set(name, { path: doc.path, canonicalBody: doc.body });
|
|
@@ -74,27 +75,36 @@ async function defaultListMemoryDocs(): Promise<MemoryListItem[]> {
|
|
|
74
75
|
return items;
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
function defaultReadMemoryDoc(name: string): { canonicalBody: string; executionBody: string } {
|
|
78
|
+
function defaultReadMemoryDoc(name: string): { path: string; canonicalBody: string; executionBody: string } {
|
|
78
79
|
const resolved = resolvedMemoryDocs.get(name);
|
|
79
80
|
if (resolved === undefined) throw new Error(`memory slash document was not resolved: ${name}`);
|
|
80
81
|
return {
|
|
82
|
+
path: resolved.path,
|
|
81
83
|
canonicalBody: resolved.canonicalBody,
|
|
82
84
|
executionBody: readMemoryDocContent(resolved.path),
|
|
83
85
|
};
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
function registerSlashExposure(path: string, body: string): void {
|
|
89
|
+
const nodeId = process.env["CRTR_NODE_ID"];
|
|
90
|
+
if (nodeId === undefined || nodeId === "") return;
|
|
91
|
+
const state = sharedContextExposureState(nodeId);
|
|
92
|
+
mergeContextExposureState(state, loadContextExposureState(nodeId));
|
|
93
|
+
registerDocumentExposure(exposureTarget(state, "transcript"), path, body, "content");
|
|
94
|
+
saveContextExposureState(nodeId, state);
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
function hasNodeRelativeExpansion(body: string): boolean {
|
|
87
98
|
return body.includes("$CRTR_CONTEXT_DIR") || body.includes("$CRTR_NODE_ID");
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
/**
|
|
101
|
+
/** Canonical name -> the registered slash-command name. */
|
|
91
102
|
export function slashCommandName(docName: string): string {
|
|
92
103
|
return docName.split("/").join(":");
|
|
93
104
|
}
|
|
94
105
|
|
|
95
|
-
/** Every `slash: true`
|
|
96
|
-
*
|
|
97
|
-
* without `slash` (or `slash: false`) is silently excluded. */
|
|
106
|
+
/** Every `slash: true` canonical winner, scope-precedence-deduped by name. A
|
|
107
|
+
* doc without `slash` (or `slash: false`) is silently excluded. */
|
|
98
108
|
export async function discoverSlashDocs(): Promise<SlashDoc[]> {
|
|
99
109
|
let items: MemoryListItem[];
|
|
100
110
|
try {
|
|
@@ -118,6 +128,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
118
128
|
const docs = await discoverSlashDocs();
|
|
119
129
|
for (const doc of docs) {
|
|
120
130
|
let expansion: DeterministicCommandExpansion | undefined;
|
|
131
|
+
let exposure: { path: string; body: string } | undefined;
|
|
121
132
|
let discloseExpansion = false;
|
|
122
133
|
let loadError: unknown;
|
|
123
134
|
try {
|
|
@@ -127,6 +138,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
127
138
|
commandName: doc.commandName,
|
|
128
139
|
body: body.executionBody,
|
|
129
140
|
};
|
|
141
|
+
exposure = { path: body.path, body: body.canonicalBody };
|
|
130
142
|
discloseExpansion = !hasNodeRelativeExpansion(body.canonicalBody);
|
|
131
143
|
} catch (err) {
|
|
132
144
|
loadError = err;
|
|
@@ -174,6 +186,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
174
186
|
},
|
|
175
187
|
{ triggerTurn: true },
|
|
176
188
|
);
|
|
189
|
+
if (exposure !== undefined) registerSlashExposure(exposure.path, exposure.body);
|
|
177
190
|
},
|
|
178
191
|
} as Parameters<ExtensionAPI["registerCommand"]>[1] & { expansion?: DeterministicCommandExpansion } & ChatCommandMetadata);
|
|
179
192
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Regression: a viewer pane lost `@crtr_node` and `@crtr_attach` while a live
|
|
2
|
+
// viewer was running in it, so `surface reload` and viewer focus refused the
|
|
3
|
+
// pane (`no_viewer`, exit 2) until it was re-tagged by hand.
|
|
4
|
+
//
|
|
5
|
+
// tmux `respawn-pane -k` SIGHUPs the outgoing viewer and starts the replacement
|
|
6
|
+
// ~50ms later without waiting for it. Measured on this machine: SIGHUP at t+0,
|
|
7
|
+
// successor running at t+49ms. An outgoing viewer whose event loop was busy
|
|
8
|
+
// when the signal landed therefore ran its teardown AFTER the successor had
|
|
9
|
+
// claimed the pane, and cleared the successor's tags.
|
|
10
|
+
// Run with: node --conditions=crtr-src --import tsx/esm --test 'src/clients/attach/__tests__/pane-tag-successor.test.ts'
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { createPaneTag } from '../session/pane-tag.js';
|
|
14
|
+
const PANE = '%474';
|
|
15
|
+
const NODE = '3zl47w7d-mt0chixr-cd2fc29b';
|
|
16
|
+
function fakeTmux() {
|
|
17
|
+
const options = new Map();
|
|
18
|
+
let stampCounter = 1_000;
|
|
19
|
+
const io = (pid) => ({
|
|
20
|
+
readOption: (_pane, name) => options.get(name) ?? '',
|
|
21
|
+
writeOption: (_pane, name, value) => { options.set(name, value); },
|
|
22
|
+
stampAsync: () => { options.set('@crtr_viewer_at', String(++stampCounter)); },
|
|
23
|
+
currentPane: () => PANE,
|
|
24
|
+
ownerId: () => pid,
|
|
25
|
+
});
|
|
26
|
+
return { options, io };
|
|
27
|
+
}
|
|
28
|
+
test('a late teardown does not clear the tags of the viewer that replaced it', () => {
|
|
29
|
+
const { options, io } = fakeTmux();
|
|
30
|
+
const outgoing = createPaneTag(false, NODE, io('4001'));
|
|
31
|
+
outgoing.set();
|
|
32
|
+
// The replacement claims the pane while the outgoing viewer is still busy.
|
|
33
|
+
const successor = createPaneTag(false, NODE, io('4002'));
|
|
34
|
+
successor.set();
|
|
35
|
+
outgoing.clear();
|
|
36
|
+
assert.equal(options.get('@crtr_node'), NODE, 'live viewer kept its node tag');
|
|
37
|
+
assert.equal(options.get('@crtr_attach'), '1', 'live viewer kept its attach tag');
|
|
38
|
+
assert.notEqual(options.get('@crtr_viewer_at'), '', 'live viewer kept its stamp');
|
|
39
|
+
});
|
|
40
|
+
test('an ordinary teardown still clears the tags it owns', () => {
|
|
41
|
+
const { options, io } = fakeTmux();
|
|
42
|
+
const viewer = createPaneTag(false, NODE, io('4001'));
|
|
43
|
+
viewer.set();
|
|
44
|
+
viewer.clear();
|
|
45
|
+
assert.equal(options.get('@crtr_node'), '');
|
|
46
|
+
assert.equal(options.get('@crtr_attach'), '');
|
|
47
|
+
assert.equal(options.get('@crtr_viewer_at'), '');
|
|
48
|
+
});
|
|
49
|
+
test('a focus refresh does not cost a viewer its own claim', () => {
|
|
50
|
+
const { options, io } = fakeTmux();
|
|
51
|
+
const viewer = createPaneTag(false, NODE, io('4001'));
|
|
52
|
+
viewer.set();
|
|
53
|
+
viewer.stamp(); // focus refresh
|
|
54
|
+
viewer.clear();
|
|
55
|
+
assert.equal(options.get('@crtr_node'), '', 'the pane it still owns is cleared');
|
|
56
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { relatedNodeDirs } from '../session/profile-files.js';
|
|
4
|
+
test('relatedNodeDirs includes a node family but not unrelated nodes', () => {
|
|
5
|
+
const nodes = [
|
|
6
|
+
{ node_id: 'root', cwd: '/work/root', parent: null },
|
|
7
|
+
{ node_id: 'parent', cwd: '/work/parent', parent: 'root' },
|
|
8
|
+
{ node_id: 'current', cwd: '/work/current', parent: 'parent' },
|
|
9
|
+
{ node_id: 'child', cwd: '/work/child', parent: 'current' },
|
|
10
|
+
{ node_id: 'grandchild', cwd: '/work/grandchild', parent: 'child' },
|
|
11
|
+
{ node_id: 'sibling', cwd: '/work/sibling', parent: 'parent' },
|
|
12
|
+
{ node_id: 'unrelated', cwd: '/work/unrelated', parent: null },
|
|
13
|
+
];
|
|
14
|
+
assert.deepEqual(relatedNodeDirs(nodes, 'current'), [
|
|
15
|
+
'/work/root',
|
|
16
|
+
'/work/parent',
|
|
17
|
+
'/work/current',
|
|
18
|
+
'/work/child',
|
|
19
|
+
'/work/grandchild',
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
@@ -91,16 +91,20 @@ export function createInboxStrip(s, hooks) {
|
|
|
91
91
|
// Same pending merge the inbox surface does — inbox-projected tickets plus
|
|
92
92
|
// reply-bearing no-inbox pages — but for every subtree node, then scoped to
|
|
93
93
|
// tickets whose origin is in the subtree.
|
|
94
|
+
// ONE page-history walk for the whole subtree, filtered by origin here.
|
|
95
|
+
// Calling `scanPageHistory(id)` per node re-walked every ticket directory
|
|
96
|
+
// once per subtree node — the same O(all tickets) scan repeated N times for
|
|
97
|
+
// a result that is just the union over subtree origins.
|
|
94
98
|
const merged = new Map();
|
|
95
99
|
for (const item of scanInbox())
|
|
96
100
|
merged.set(item.dir, item);
|
|
97
|
-
for (const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
for (const page of scanPageHistory()) {
|
|
102
|
+
if (page.source.nodeId === undefined || !nodes.has(page.source.nodeId))
|
|
103
|
+
continue;
|
|
104
|
+
if (page.state !== 'pending' || !page.awaitsResponse || page.inbox)
|
|
105
|
+
continue;
|
|
106
|
+
if (!merged.has(page.dir))
|
|
107
|
+
merged.set(page.dir, page);
|
|
104
108
|
}
|
|
105
109
|
tickets = [...merged.values()]
|
|
106
110
|
.filter((t) => t.source.nodeId !== undefined && nodes.has(t.source.nodeId))
|
|
@@ -5,13 +5,39 @@
|
|
|
5
5
|
* Split into a claim + a clear because ordering matters: a remote relay's "no
|
|
6
6
|
* node"/"no running broker" close can still throw AFTER the socket is up, so
|
|
7
7
|
* the tag is claimed only past that checkpoint — while `clear()` must be safe
|
|
8
|
-
* to call from a teardown that runs before it was ever claimed.
|
|
9
|
-
|
|
8
|
+
* to call from a teardown that runs before it was ever claimed.
|
|
9
|
+
*
|
|
10
|
+
* `clear()` also has to survive a SUCCESSOR already owning the pane. tmux
|
|
11
|
+
* `respawn-pane -k` only SIGHUPs the old viewer — it does not wait for it, and
|
|
12
|
+
* the replacement is running ~50ms later. A departing viewer whose event loop
|
|
13
|
+
* was busy when the signal arrived therefore runs its teardown after the
|
|
14
|
+
* replacement has claimed the pane, and an unconditional clear then wipes the
|
|
15
|
+
* LIVE viewer's identity: `nodeInPane()` returns nothing, so `surface reload`
|
|
16
|
+
* and viewer focus refuse the pane until someone re-tags it by hand. So the
|
|
17
|
+
* clear is conditional on still owning the pane, decided by `@crtr_viewer_pid`:
|
|
18
|
+
* a pane carrying someone else's pid has a successor, and its tags are not
|
|
19
|
+
* ours to remove. Skipping leaves at worst a stale tag on a dead pane, which
|
|
20
|
+
* `liveViewerPanes` and `surface reload` already validate against the live
|
|
21
|
+
* process; wiping a live viewer's tag has no such defense. */
|
|
22
|
+
/** The tmux reads and writes the tag lifecycle makes, injectable so the
|
|
23
|
+
* successor race is testable without a live tmux server. */
|
|
24
|
+
export interface PaneTagIo {
|
|
25
|
+
readOption: (pane: string, name: string) => string | undefined;
|
|
26
|
+
writeOption: (pane: string, name: string, value: string) => void;
|
|
27
|
+
stampAsync: (pane: string) => void;
|
|
28
|
+
currentPane: () => string | undefined;
|
|
29
|
+
/** This viewer process's identity — the pid, unique per viewer by construction. */
|
|
30
|
+
ownerId: () => string;
|
|
31
|
+
}
|
|
32
|
+
export declare function createPaneTag(remote: boolean, nodeId: string, io?: PaneTagIo): {
|
|
10
33
|
set: () => void;
|
|
11
34
|
stamp: () => void;
|
|
12
35
|
clear: () => void;
|
|
13
36
|
pane: () => string | undefined;
|
|
14
37
|
};
|
|
38
|
+
/** Which viewer process owns this pane's tags. Read only by `clear()`, so a
|
|
39
|
+
* departing viewer never removes a successor's identity. */
|
|
40
|
+
export declare const VIEWER_OWNER_OPTION = "@crtr_viewer_pid";
|
|
15
41
|
export declare function tagAttachPane(remote: boolean, pane: string | undefined, nodeId: string, setOpt?: (pane: string, name: string, value: string) => void): void;
|
|
16
42
|
export declare function untagAttachPane(remote: boolean, pane: string | undefined, setOpt?: (pane: string, name: string, value: string) => void): void;
|
|
17
43
|
export declare function tagAttachViewerPane(pane: string | undefined, setOpt?: (pane: string, name: string, value: string) => void): void;
|
|
@@ -5,50 +5,76 @@
|
|
|
5
5
|
// local id) from that pane. `@crtr_attach` carries no identity: every local or
|
|
6
6
|
// remote attach viewer sets it so tmux can conditionally forward opted-in root
|
|
7
7
|
// gestures into whichever attach pane currently has focus.
|
|
8
|
-
import { currentTmux, setPaneOption, stampViewerPaneAsync, VIEWER_STAMP_OPTION, } from '../../../core/runtime/placement-tmux.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export function createPaneTag(remote, nodeId) {
|
|
8
|
+
import { currentTmux, getPaneOption, setPaneOption, stampViewerPaneAsync, VIEWER_STAMP_OPTION, } from '../../../core/runtime/placement-tmux.js';
|
|
9
|
+
const realPaneTagIo = {
|
|
10
|
+
readOption: getPaneOption,
|
|
11
|
+
writeOption: (pane, name, value) => { setPaneOption(pane, name, value); },
|
|
12
|
+
stampAsync: stampViewerPaneAsync,
|
|
13
|
+
currentPane: () => process.env['TMUX_PANE'] ?? currentTmux()?.pane,
|
|
14
|
+
ownerId: () => String(process.pid),
|
|
15
|
+
};
|
|
16
|
+
export function createPaneTag(remote, nodeId, io = realPaneTagIo) {
|
|
18
17
|
let pane;
|
|
18
|
+
const ownerPid = io.ownerId();
|
|
19
|
+
/** False once another viewer's pid is on the pane. An unclaimed pane (no pid
|
|
20
|
+
* recorded — a viewer that never reached `set()`, or one from a generation
|
|
21
|
+
* that predates this option) keeps the old unconditional behavior. */
|
|
22
|
+
const stillOwnsPane = () => {
|
|
23
|
+
if (pane === undefined || pane === '')
|
|
24
|
+
return true;
|
|
25
|
+
const claimed = io.readOption(pane, VIEWER_OWNER_OPTION);
|
|
26
|
+
if (claimed === undefined || claimed === '')
|
|
27
|
+
return true;
|
|
28
|
+
return claimed === ownerPid;
|
|
29
|
+
};
|
|
19
30
|
return {
|
|
20
31
|
set: () => {
|
|
21
|
-
pane =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
pane = io.currentPane();
|
|
33
|
+
claimAttachPane(pane, ownerPid, io.writeOption);
|
|
34
|
+
tagAttachPane(remote, pane, nodeId, io.writeOption);
|
|
35
|
+
tagAttachViewerPane(pane, io.writeOption);
|
|
36
|
+
stampAttachPane(remote, pane, io.writeOption);
|
|
25
37
|
},
|
|
26
38
|
stamp: () => {
|
|
27
39
|
if (remote || pane === undefined || pane === '')
|
|
28
40
|
return;
|
|
29
|
-
|
|
41
|
+
io.stampAsync(pane);
|
|
30
42
|
},
|
|
31
43
|
clear: () => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
if (!stillOwnsPane())
|
|
45
|
+
return;
|
|
46
|
+
untagAttachPane(remote, pane, io.writeOption);
|
|
47
|
+
untagAttachViewerPane(pane, io.writeOption);
|
|
48
|
+
unstampAttachPane(remote, pane, io.writeOption);
|
|
49
|
+
claimAttachPane(pane, '', io.writeOption);
|
|
35
50
|
},
|
|
36
51
|
pane: () => pane,
|
|
37
52
|
};
|
|
38
53
|
}
|
|
39
|
-
|
|
54
|
+
/** Which viewer process owns this pane's tags. Read only by `clear()`, so a
|
|
55
|
+
* departing viewer never removes a successor's identity. */
|
|
56
|
+
export const VIEWER_OWNER_OPTION = '@crtr_viewer_pid';
|
|
57
|
+
function claimAttachPane(pane, ownerPid, setOpt) {
|
|
58
|
+
if (pane === undefined || pane === '')
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
setOpt(pane, VIEWER_OWNER_OPTION, ownerPid);
|
|
62
|
+
}
|
|
63
|
+
catch { /* best-effort */ }
|
|
64
|
+
}
|
|
65
|
+
function stampAttachPane(remote, pane, setOpt) {
|
|
40
66
|
if (remote || pane === undefined || pane === '')
|
|
41
67
|
return;
|
|
42
68
|
try {
|
|
43
|
-
|
|
69
|
+
setOpt(pane, VIEWER_STAMP_OPTION, String(Date.now()));
|
|
44
70
|
}
|
|
45
71
|
catch { /* best-effort */ }
|
|
46
72
|
}
|
|
47
|
-
function unstampAttachPane(remote, pane) {
|
|
73
|
+
function unstampAttachPane(remote, pane, setOpt = setPaneOption) {
|
|
48
74
|
if (remote || pane === undefined || pane === '')
|
|
49
75
|
return;
|
|
50
76
|
try {
|
|
51
|
-
|
|
77
|
+
setOpt(pane, VIEWER_STAMP_OPTION, '');
|
|
52
78
|
}
|
|
53
79
|
catch { /* best-effort */ }
|
|
54
80
|
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import type { NodeRow } from '../../../core/canvas/types.js';
|
|
1
2
|
interface ProfileFileSearchOptions {
|
|
2
3
|
cwd: string;
|
|
4
|
+
nodeDirs: readonly string[];
|
|
3
5
|
profileId: string | null;
|
|
4
6
|
fdPath: string | null;
|
|
5
7
|
onNotice: (message: string) => void;
|
|
6
8
|
}
|
|
9
|
+
/** Return the working directories for this node, its complete parent chain,
|
|
10
|
+
* and its full descendant subtree. The `parent` hierarchy, rather than the
|
|
11
|
+
* subscription graph, is the user-visible child/parent relationship. */
|
|
12
|
+
export declare function relatedNodeDirs(nodes: readonly Pick<NodeRow, 'node_id' | 'cwd' | 'parent'>[], nodeId: string): string[];
|
|
7
13
|
export declare function launchProfileFileSearch(opts: ProfileFileSearchOptions): void;
|
|
8
14
|
export {};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// session/profile-files.ts — fuzzy file search across a node's local profile.
|
|
1
|
+
// session/profile-files.ts — fuzzy file search across a node's working family and local profile.
|
|
2
2
|
//
|
|
3
|
-
// The popup searches the current workspace, every
|
|
4
|
-
//
|
|
5
|
-
// `.crouter/memory/` stores are added with
|
|
6
|
-
// memory remains findable even when a parent
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// The popup searches the current workspace, every ancestor and descendant
|
|
4
|
+
// node's workspace, every project named by the node's profile manifest, and the
|
|
5
|
+
// profile's own memory store. Ancestor `.crouter/memory/` stores are added with
|
|
6
|
+
// ignore rules disabled, so project memory remains findable even when a parent
|
|
7
|
+
// ignore file hides `.crouter/`. fzf owns matching and selection and nvim owns
|
|
8
|
+
// viewing; the attach viewer resolves the related nodes and wires the two popups.
|
|
9
9
|
import { constants, accessSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
10
10
|
import { tmpdir } from 'node:os';
|
|
11
11
|
import { delimiter, dirname, join, resolve, sep } from 'node:path';
|
|
@@ -13,6 +13,31 @@ import { execFile, spawnSync } from 'node:child_process';
|
|
|
13
13
|
import { loadProfileManifest, profileMemoryDir } from '../../../core/profiles/manifest.js';
|
|
14
14
|
import { shellQuote } from '../../../core/runtime/placement-tmux.js';
|
|
15
15
|
import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
|
|
16
|
+
/** Return the working directories for this node, its complete parent chain,
|
|
17
|
+
* and its full descendant subtree. The `parent` hierarchy, rather than the
|
|
18
|
+
* subscription graph, is the user-visible child/parent relationship. */
|
|
19
|
+
export function relatedNodeDirs(nodes, nodeId) {
|
|
20
|
+
const byId = new Map(nodes.map((node) => [node.node_id, node]));
|
|
21
|
+
const included = new Set([nodeId]);
|
|
22
|
+
for (let current = byId.get(nodeId); current?.parent !== null && current?.parent !== undefined;) {
|
|
23
|
+
const parentId = current.parent;
|
|
24
|
+
if (included.has(parentId))
|
|
25
|
+
break;
|
|
26
|
+
included.add(parentId);
|
|
27
|
+
current = byId.get(parentId);
|
|
28
|
+
}
|
|
29
|
+
const pending = [nodeId];
|
|
30
|
+
while (pending.length > 0) {
|
|
31
|
+
const parentId = pending.pop();
|
|
32
|
+
for (const child of nodes) {
|
|
33
|
+
if (child.parent !== parentId || included.has(child.node_id))
|
|
34
|
+
continue;
|
|
35
|
+
included.add(child.node_id);
|
|
36
|
+
pending.push(child.node_id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return nodes.filter((node) => included.has(node.node_id)).map((node) => node.cwd);
|
|
40
|
+
}
|
|
16
41
|
function isDirectory(path) {
|
|
17
42
|
try {
|
|
18
43
|
return statSync(path).isDirectory();
|
|
@@ -93,6 +118,9 @@ function ancestorMemoryDirs(pointers) {
|
|
|
93
118
|
}
|
|
94
119
|
return dedupeExistingDirs(found);
|
|
95
120
|
}
|
|
121
|
+
/** Columns fzf keeps to the right of the matched substring when it scrolls a
|
|
122
|
+
* long path into view. Wide enough to show the rest of the filename. */
|
|
123
|
+
const HSCROLL_OFF = 20;
|
|
96
124
|
function selectionPath(path) {
|
|
97
125
|
try {
|
|
98
126
|
const bytes = readFileSync(path);
|
|
@@ -122,7 +150,7 @@ export function launchProfileFileSearch(opts) {
|
|
|
122
150
|
return;
|
|
123
151
|
}
|
|
124
152
|
const profile = profileSearchRoots(opts.profileId);
|
|
125
|
-
const pointers = dedupeExistingDirs([opts.cwd, ...profile.projects]);
|
|
153
|
+
const pointers = dedupeExistingDirs([opts.cwd, ...opts.nodeDirs, ...profile.projects]);
|
|
126
154
|
if (pointers.length === 0) {
|
|
127
155
|
opts.onNotice('No profile file-search directories exist');
|
|
128
156
|
return;
|
|
@@ -134,11 +162,16 @@ export function launchProfileFileSearch(opts) {
|
|
|
134
162
|
]);
|
|
135
163
|
const tempDir = mkdtempSync(join(tmpdir(), 'crtr-profile-files-'));
|
|
136
164
|
const selected = join(tempDir, 'selection');
|
|
165
|
+
const candidates = join(tempDir, 'candidates');
|
|
137
166
|
const scans = [
|
|
138
|
-
...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')}
|
|
139
|
-
...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore
|
|
167
|
+
...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')} . ${shellQuote(root)}`),
|
|
168
|
+
...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore . ${shellQuote(memory)}`),
|
|
140
169
|
];
|
|
141
|
-
|
|
170
|
+
// Every line is the whole path and nothing else, so fzf matches, ranks, and
|
|
171
|
+
// horizontally scrolls against the same text — a hidden field would be
|
|
172
|
+
// unsearchable, and a display slice would silently narrow the query.
|
|
173
|
+
const yankPath = 'printf \'%s\' {} | { if command -v pbcopy >/dev/null 2>&1; then pbcopy; elif command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard; else cat >/dev/null; fi; }; printf \'%s\' {} | tmux load-buffer -w -';
|
|
174
|
+
const command = `{ ${scans.map((scan) => `${scan} &`).join(' ')} wait; } > ${shellQuote(candidates)} && ${shellQuote(fzf)} --print0 --scheme=path --hscroll-off=${HSCROLL_OFF} --header ${shellQuote('fuzzy path • ctrl-y yank file path')} --bind ${shellQuote(`ctrl-y:execute-silent(${yankPath})`)} --preview ${shellQuote('head -100 -- {}')} < ${shellQuote(candidates)} > ${shellQuote(selected)}`;
|
|
142
175
|
try {
|
|
143
176
|
execFile('tmux', ['display-popup', '-E', '-T', ' profile files ', '-w', '80%', '-h', '60%', ...surfaceTmuxStyleArgs(), command], () => {
|
|
144
177
|
const file = selectionPath(selected);
|
|
@@ -396,11 +396,8 @@ function dispatchCanvasCommand(name, arg, ctx) {
|
|
|
396
396
|
return true;
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
|
-
/** `/promote [kind]`
|
|
400
|
-
*
|
|
401
|
-
* this node (out-of-process), notifies the result, then triggers a turn so the
|
|
402
|
-
* persona injector steers in the new-role guidance at the turn boundary — exactly
|
|
403
|
-
* as the node would by running the command itself. */
|
|
399
|
+
/** `/promote [kind]` shells the out-of-process mutation, reports it, then
|
|
400
|
+
* triggers a turn whose provider gate appends the pending transition context. */
|
|
404
401
|
function promoteNode(arg, ctx) {
|
|
405
402
|
const nodeId = nodeIdFor(ctx).trim();
|
|
406
403
|
if (nodeId === '') {
|
|
@@ -446,8 +443,8 @@ function promoteNode(arg, ctx) {
|
|
|
446
443
|
}
|
|
447
444
|
const rmPath = (result.roadmap_path ?? '').trim();
|
|
448
445
|
ctx.notify(`Promoted to ${result.kind ?? 'orchestrator'} orchestrator — authoring roadmap${rmPath !== '' ? ` (${rmPath})` : ''}.`);
|
|
449
|
-
// Trigger a turn so the
|
|
450
|
-
//
|
|
446
|
+
// Trigger a turn so the pending transition lands before the promoted
|
|
447
|
+
// node's response; the viewer's only turn trigger is `prompt`.
|
|
451
448
|
ctx.send({
|
|
452
449
|
type: 'prompt',
|
|
453
450
|
text: 'You have just been promoted to an orchestrator. Your new-role guidance is arriving — read it, author your roadmap, and start delegating.',
|