nawabari 0.9.2 → 0.10.2

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.
Files changed (75) hide show
  1. package/README.md +142 -803
  2. package/dist/cli-command-registry.js +46 -6
  3. package/dist/cli-command-registry.js.map +1 -1
  4. package/dist/cli.d.ts +5 -3
  5. package/dist/cli.js +157 -34
  6. package/dist/cli.js.map +1 -1
  7. package/dist/contract.js +11 -3
  8. package/dist/contract.js.map +1 -1
  9. package/dist/domain/compatibility-runtime-projection.d.ts +24 -0
  10. package/dist/domain/compatibility-runtime-projection.js +197 -0
  11. package/dist/domain/compatibility-runtime-projection.js.map +1 -0
  12. package/dist/domain/doctor.d.ts +2 -2
  13. package/dist/domain/doctor.js +72 -51
  14. package/dist/domain/doctor.js.map +1 -1
  15. package/dist/domain/errors.d.ts +1 -1
  16. package/dist/domain/errors.js +5 -1
  17. package/dist/domain/errors.js.map +1 -1
  18. package/dist/domain/fhs-development-runtime.d.ts +90 -0
  19. package/dist/domain/fhs-development-runtime.js +385 -0
  20. package/dist/domain/fhs-development-runtime.js.map +1 -0
  21. package/dist/domain/fhs-runtime.d.ts +36 -0
  22. package/dist/domain/fhs-runtime.js +776 -0
  23. package/dist/domain/fhs-runtime.js.map +1 -0
  24. package/dist/domain/landlock.d.ts +11 -4
  25. package/dist/domain/landlock.js +44 -21
  26. package/dist/domain/landlock.js.map +1 -1
  27. package/dist/domain/nix-runtime-closure.d.ts +70 -0
  28. package/dist/domain/nix-runtime-closure.js +449 -0
  29. package/dist/domain/nix-runtime-closure.js.map +1 -0
  30. package/dist/domain/pnpm-middleware-backend-materialization.d.ts +117 -0
  31. package/dist/domain/pnpm-middleware-backend-materialization.js +311 -0
  32. package/dist/domain/pnpm-middleware-backend-materialization.js.map +1 -0
  33. package/dist/domain/runtime-executable-projection.d.ts +33 -0
  34. package/dist/domain/runtime-executable-projection.js +204 -0
  35. package/dist/domain/runtime-executable-projection.js.map +1 -0
  36. package/dist/domain/runtime-file-identity.d.ts +37 -0
  37. package/dist/domain/runtime-file-identity.js +114 -0
  38. package/dist/domain/runtime-file-identity.js.map +1 -0
  39. package/dist/domain/runtime-profile.d.ts +66 -0
  40. package/dist/domain/runtime-profile.js +356 -0
  41. package/dist/domain/runtime-profile.js.map +1 -0
  42. package/dist/domain/runtime-projection.d.ts +101 -0
  43. package/dist/domain/runtime-projection.js +316 -0
  44. package/dist/domain/runtime-projection.js.map +1 -0
  45. package/dist/domain/runtime-provider-pnpm-middleware.d.ts +41 -0
  46. package/dist/domain/runtime-provider-pnpm-middleware.js +316 -0
  47. package/dist/domain/runtime-provider-pnpm-middleware.js.map +1 -0
  48. package/dist/domain/runtime-provider-tgrep.d.ts +60 -0
  49. package/dist/domain/runtime-provider-tgrep.js +537 -0
  50. package/dist/domain/runtime-provider-tgrep.js.map +1 -0
  51. package/dist/domain/runtime-resolution.d.ts +57 -0
  52. package/dist/domain/runtime-resolution.js +242 -0
  53. package/dist/domain/runtime-resolution.js.map +1 -0
  54. package/dist/domain/sandbox-launcher.d.ts +7 -0
  55. package/dist/domain/sandbox-launcher.js +380 -56
  56. package/dist/domain/sandbox-launcher.js.map +1 -1
  57. package/dist/domain/sandbox.d.ts +59 -4
  58. package/dist/domain/sandbox.js +100 -5
  59. package/dist/domain/sandbox.js.map +1 -1
  60. package/dist/domain/session-backend.js +20 -1
  61. package/dist/domain/session-backend.js.map +1 -1
  62. package/dist/domain/session.d.ts +3 -2
  63. package/dist/domain/session.js.map +1 -1
  64. package/dist/domain/tgrep-runtime-materialization.d.ts +61 -0
  65. package/dist/domain/tgrep-runtime-materialization.js +172 -0
  66. package/dist/domain/tgrep-runtime-materialization.js.map +1 -0
  67. package/dist/operation-authorization.d.ts +10 -0
  68. package/dist/operation-authorization.js +12 -1
  69. package/dist/operation-authorization.js.map +1 -1
  70. package/dist/presentation.js +6 -3
  71. package/dist/presentation.js.map +1 -1
  72. package/dist/session-registry.d.ts +11 -3
  73. package/dist/session-registry.js +61 -45
  74. package/dist/session-registry.js.map +1 -1
  75. package/package.json +3 -1
@@ -222,12 +222,19 @@ export type GarbageCollectEligibility = "eligible" | "ineligible" | "ambiguous";
222
222
  export type GarbageCollectSuspicionReason = "none" | "elapsed-age" | "explicit-lifecycle-state" | "missing-worktree" | "ambiguous-physical-state";
223
223
  export type GarbageCollectEligibilityReason = "not-suspected" | "age-only" | "explicit-stale-state" | "explicit-closing-state" | "prunable-missing-worktree" | "physical-state-ambiguous";
224
224
  /** Bounded machine-readable GC assessment. Session identity stays flat for API compatibility. */
225
- export interface GarbageCollectCandidate extends SessionRecord {
225
+ export interface GarbageCollectAssessment extends SessionRecord {
226
226
  readonly physicalState: string;
227
227
  readonly suspicion: GarbageCollectSuspicion;
228
228
  readonly suspicionReason: GarbageCollectSuspicionReason;
229
229
  readonly destructiveEligibility: GarbageCollectEligibility;
230
230
  readonly destructiveEligibilityReason: GarbageCollectEligibilityReason;
231
+ }
232
+ /**
233
+ * A GC result adds lifecycle actions to the assessment. Diagnostics use the
234
+ * assessment alone so inspect has one canonical lifecycle projection rather
235
+ * than a copied nested projection.
236
+ */
237
+ export interface GarbageCollectCandidate extends GarbageCollectAssessment {
231
238
  /** Canonical read-only lifecycle projection for this GC observation. */
232
239
  readonly lifecycle?: SessionLifecycleClassification;
233
240
  /** Typed caller actions projected from the same lifecycle snapshot. */
@@ -312,7 +319,7 @@ export interface SessionDiagnostic {
312
319
  /** All bounded actions available for the observed state, in deterministic order. */
313
320
  readonly nextActions: readonly SessionLifecycleAction[];
314
321
  readonly integrationEvidence: SessionDiagnosticIntegrationEvidence;
315
- readonly garbageCollection: GarbageCollectCandidate;
322
+ readonly garbageCollection: GarbageCollectAssessment;
316
323
  /** Canonical termination/recovery projection; additive for old consumers. */
317
324
  readonly lifecycle?: SessionLifecycleClassification;
318
325
  }
@@ -619,12 +626,13 @@ export declare class SessionRegistry {
619
626
  private readonly protectedBranchNames;
620
627
  private readonly protectedWorktreePaths;
621
628
  private readonly worktreeRoot;
629
+ private readonly worktreeRootIsDefault;
622
630
  private readonly staleAfterMs;
623
631
  private readonly lockStaleAfterMs;
624
632
  private readonly lockMetadataGraceMs;
625
633
  private readonly lock;
626
634
  constructor(options?: SessionRegistryOptions);
627
- /** Resolved repository-local root used for default managed session worktrees. */
635
+ /** Resolved managed root used for default session worktrees. */
628
636
  get managedWorktreeRoot(): string;
629
637
  read(): readonly SessionRecord[];
630
638
  list(): readonly SessionRecord[];
@@ -75,6 +75,7 @@ export class SessionRegistry {
75
75
  protectedBranchNames;
76
76
  protectedWorktreePaths;
77
77
  worktreeRoot;
78
+ worktreeRootIsDefault;
78
79
  staleAfterMs;
79
80
  lockStaleAfterMs;
80
81
  lockMetadataGraceMs;
@@ -88,7 +89,14 @@ export class SessionRegistry {
88
89
  this.defaultBranchName = options.defaultBranchName;
89
90
  this.protectedBranchNames = Object.freeze([...(options.protectedBranchNames ?? [])]);
90
91
  this.protectedWorktreePaths = Object.freeze([...(options.protectedWorktreePaths ?? [])]);
91
- this.worktreeRoot = resolveManagedWorktreeRoot(options.worktreeRoot ?? path.dirname(this.repository.worktreePath));
92
+ const configuredWorktreeRoot = options.worktreeRoot;
93
+ this.worktreeRootIsDefault = configuredWorktreeRoot === undefined;
94
+ if (configuredWorktreeRoot === undefined) {
95
+ this.worktreeRoot = defaultManagedWorktreeRoot(this.repository.worktreePath);
96
+ }
97
+ else {
98
+ this.worktreeRoot = resolveManagedWorktreeRoot(configuredWorktreeRoot);
99
+ }
92
100
  this.staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
93
101
  this.lockStaleAfterMs = options.lockStaleAfterMs ?? this.lockTimeoutMs;
94
102
  this.lockMetadataGraceMs = options.lockMetadataGraceMs ?? DEFAULT_LOCK_METADATA_GRACE_MS;
@@ -117,7 +125,7 @@ export class SessionRegistry {
117
125
  metadataGraceMs: this.lockMetadataGraceMs,
118
126
  });
119
127
  }
120
- /** Resolved repository-local root used for default managed session worktrees. */
128
+ /** Resolved managed root used for default session worktrees. */
121
129
  get managedWorktreeRoot() {
122
130
  return this.worktreeRoot;
123
131
  }
@@ -1753,8 +1761,8 @@ export class SessionRegistry {
1753
1761
  if (assessment.terminalOperation === "discard")
1754
1762
  return false;
1755
1763
  // Eligibility is evaluated from the GC authority's own bounded
1756
- // evidence. Full cleanup blockers remain on `assessment.lifecycle`
1757
- // and are applied by the existing preflight below.
1764
+ // evidence. Full cleanup blockers are applied by the existing
1765
+ // preflight below.
1758
1766
  const gcLifecycle = projectGarbageCollectAuthorization(assessment);
1759
1767
  if (assessment.destructiveEligibility === "eligible" &&
1760
1768
  (gcLifecycle.destructiveCleanupEligible || assessment.physicalState === "prunable-missing")) {
@@ -2624,8 +2632,7 @@ export class SessionRegistry {
2624
2632
  phase: "termination",
2625
2633
  });
2626
2634
  const nextActions = projectSessionLifecycleActions({ classification: lifecycle, sessionId: record.sessionId });
2627
- const garbageCollection = closedGarbageCollectionAssessment(record, now);
2628
- const projectedGarbageCollection = projectGarbageCollectCandidateLifecycle(garbageCollection, lifecycle, []);
2635
+ const garbageCollection = closedGarbageCollectionAssessment(record);
2629
2636
  return Object.freeze({
2630
2637
  schemaVersion: SESSION_DIAGNOSTIC_SCHEMA_VERSION,
2631
2638
  operation: "diagnostic",
@@ -2644,7 +2651,7 @@ export class SessionRegistry {
2644
2651
  ...(nextActions.length === 0 ? {} : { nextAction: nextActions[0] }),
2645
2652
  nextActions,
2646
2653
  integrationEvidence: Object.freeze(integrationEvidence),
2647
- garbageCollection: projectedGarbageCollection,
2654
+ garbageCollection,
2648
2655
  lifecycle,
2649
2656
  });
2650
2657
  }
@@ -2677,7 +2684,6 @@ export class SessionRegistry {
2677
2684
  ageSuspicious: Date.parse(now) - Date.parse(record.updatedAt) >= this.staleAfterMs,
2678
2685
  phase: "termination",
2679
2686
  });
2680
- garbageCollection = projectGarbageCollectCandidateLifecycle(garbageCollection, lifecycle, blockers);
2681
2687
  const safeActions = blockers.length > 0
2682
2688
  ? sortStrings(Array.from(new Set(blockers.flatMap((blocker) => blocker.safeActions))))
2683
2689
  : closeReadiness === "ready"
@@ -3019,7 +3025,17 @@ export class SessionRegistry {
3019
3025
  : options.worktreeRoot !== undefined
3020
3026
  ? resolveManagedWorktreeRoot(options.worktreeRoot)
3021
3027
  : this.worktreeRoot;
3022
- const requestedWorktreePath = resolveProvisionedWorktreePath(options.worktreePath ?? path.join(effectiveRoot, `${path.basename(this.repository.worktreePath)}-${sessionId}`), effectiveRoot);
3028
+ const legacyRoot = path.dirname(this.repository.worktreePath);
3029
+ const legacyCompatibilityEnabled = this.worktreeRootIsDefault && options.worktreeRoot === undefined;
3030
+ const defaultRootSelected = legacyCompatibilityEnabled &&
3031
+ (options.worktreePath === undefined ||
3032
+ !path.isAbsolute(options.worktreePath) ||
3033
+ isPathInside(this.worktreeRoot, path.resolve(options.worktreePath)));
3034
+ if (defaultRootSelected)
3035
+ ensureManagedWorktreeRoot(this.worktreeRoot);
3036
+ const requestedWorktreePath = options.worktreePath === undefined
3037
+ ? resolveProvisionedWorktreePath(path.join(effectiveRoot, `${path.basename(this.repository.worktreePath)}-${sessionId}`), effectiveRoot)
3038
+ : resolveProvisionedWorktreePathWithLegacyCompatibility(options.worktreePath, effectiveRoot, legacyCompatibilityEnabled ? legacyRoot : undefined);
3023
3039
  const defaultWorktreePath = worktrees.find((worktree) => !worktree.prunable)?.worktreePath ?? this.repository.worktreePath;
3024
3040
  const configuredProtectedWorktrees = [
3025
3041
  ...this.protectedWorktreePaths,
@@ -3359,17 +3375,6 @@ function assessGarbageCollection(record, now, staleAfterMs, worktrees) {
3359
3375
  destructiveEligibility = "ineligible";
3360
3376
  destructiveEligibilityReason = "age-only";
3361
3377
  }
3362
- const lifecycle = projectGarbageCollectAuthorization({
3363
- state: record.state,
3364
- physicalState: worktreeState.kind,
3365
- terminalOperation: record.terminalOperation,
3366
- suspicion,
3367
- destructiveEligibility,
3368
- });
3369
- const nextActions = projectSessionLifecycleActions({
3370
- classification: lifecycle,
3371
- sessionId: record.sessionId,
3372
- });
3373
3378
  return Object.freeze({
3374
3379
  ...cloneSessionRecord(record),
3375
3380
  physicalState: worktreeState.kind,
@@ -3377,8 +3382,6 @@ function assessGarbageCollection(record, now, staleAfterMs, worktrees) {
3377
3382
  suspicionReason,
3378
3383
  destructiveEligibility,
3379
3384
  destructiveEligibilityReason,
3380
- lifecycle,
3381
- nextActions,
3382
3385
  });
3383
3386
  }
3384
3387
  /** Evaluate GC's independent authorization through the canonical machine. */
@@ -3394,18 +3397,7 @@ function projectGarbageCollectAuthorization(candidate) {
3394
3397
  phase: "termination",
3395
3398
  });
3396
3399
  }
3397
- function closedGarbageCollectionAssessment(record, now) {
3398
- const lifecycle = classifySessionLifecycle({
3399
- sessionState: record.state,
3400
- physicalState: "closed",
3401
- closeReadiness: "ready",
3402
- terminalOperation: record.terminalOperation,
3403
- phase: "termination",
3404
- });
3405
- const nextActions = projectSessionLifecycleActions({
3406
- classification: lifecycle,
3407
- sessionId: record.sessionId,
3408
- });
3400
+ function closedGarbageCollectionAssessment(record) {
3409
3401
  return Object.freeze({
3410
3402
  ...cloneSessionRecord(record),
3411
3403
  physicalState: "closed",
@@ -3413,8 +3405,6 @@ function closedGarbageCollectionAssessment(record, now) {
3413
3405
  suspicionReason: "none",
3414
3406
  destructiveEligibility: "ineligible",
3415
3407
  destructiveEligibilityReason: "not-suspected",
3416
- lifecycle,
3417
- nextActions,
3418
3408
  });
3419
3409
  }
3420
3410
  function isDestructiveGcEligible(record, worktrees) {
@@ -3576,15 +3566,6 @@ function toCleanupBlocker(error) {
3576
3566
  recoveryHints: Object.freeze(recoveryHints),
3577
3567
  });
3578
3568
  }
3579
- /** Keep the nested GC assessment on the same observed lifecycle snapshot. */
3580
- function projectGarbageCollectCandidateLifecycle(candidate, lifecycle, blockers) {
3581
- const nextActions = projectSessionLifecycleActions({
3582
- classification: lifecycle,
3583
- sessionId: candidate.sessionId,
3584
- blockers,
3585
- });
3586
- return Object.freeze({ ...candidate, lifecycle, nextActions });
3587
- }
3588
3569
  function toDiagnosticBlocker(error) {
3589
3570
  const blocker = toCleanupBlocker(error);
3590
3571
  return Object.freeze({
@@ -3949,9 +3930,18 @@ function runMutationGit(git, args, cwd, phase, failureCode) {
3949
3930
  ...(error instanceof SessionRegistryError ? error.details : {}),
3950
3931
  ...(error instanceof SessionRegistryError ? { gitCode: error.code } : {}),
3951
3932
  };
3933
+ if (failureCode === "COMMIT_FAILED" && isMissingGitIdentityFailure(details.stderr)) {
3934
+ throw new SessionRegistryError(failureCode, `${phase} Git operation failed: Git author identity (user.name/user.email) is unavailable`, {
3935
+ ...details,
3936
+ hint: "Set repository-local identity with `git config user.name`/`git config user.email`, or configure the host's global Git identity before starting the session.",
3937
+ }, error);
3938
+ }
3952
3939
  throw new SessionRegistryError(failureCode, `${phase} Git operation failed`, details, error);
3953
3940
  }
3954
3941
  }
3942
+ function isMissingGitIdentityFailure(stderr) {
3943
+ return typeof stderr === "string" && /Please tell me who you are|unable to auto-detect email address/u.test(stderr);
3944
+ }
3955
3945
  /**
3956
3946
  * Reconcile a bounded commit transport failure against the only authoritative
3957
3947
  * local commit evidence: HEAD and the paths recorded by that HEAD. A
@@ -4467,6 +4457,23 @@ function generateUniqueSessionId(records, idGenerator) {
4467
4457
  }
4468
4458
  throw new SessionRegistryError("SESSION_ID_COLLISION", `Could not generate a unique session ID after ${MAX_ID_GENERATION_ATTEMPTS} attempts`, { attempts: MAX_ID_GENERATION_ATTEMPTS });
4469
4459
  }
4460
+ function defaultManagedWorktreeRoot(repositoryWorktreePath) {
4461
+ const resolved = path.resolve(path.dirname(repositoryWorktreePath), ".nawabari", "worktrees");
4462
+ assertNoSymlinkPath(resolved);
4463
+ return resolved;
4464
+ }
4465
+ function ensureManagedWorktreeRoot(candidate) {
4466
+ try {
4467
+ assertNoSymlinkPath(candidate);
4468
+ fs.mkdirSync(candidate, { recursive: true, mode: 0o700 });
4469
+ return resolveManagedWorktreeRoot(candidate);
4470
+ }
4471
+ catch (error) {
4472
+ if (error instanceof SessionRegistryError)
4473
+ throw error;
4474
+ throw new SessionRegistryError("INVALID_WORKTREE_PATH", `Could not create managed worktree root: ${candidate}`, { worktree: candidate }, error);
4475
+ }
4476
+ }
4470
4477
  function resolveManagedWorktreeRoot(candidate) {
4471
4478
  if (candidate.includes("\u0000") || candidate.trim().length === 0) {
4472
4479
  throw new SessionRegistryError("INVALID_WORKTREE_PATH", `Invalid managed worktree root: ${candidate}`, {
@@ -4489,6 +4496,15 @@ function resolveManagedWorktreeRoot(candidate) {
4489
4496
  throw new SessionRegistryError("INVALID_WORKTREE_PATH", `Could not resolve managed worktree root: ${resolved}`, { worktree: resolved }, error);
4490
4497
  }
4491
4498
  }
4499
+ function resolveProvisionedWorktreePathWithLegacyCompatibility(candidate, managedRoot, legacyRoot) {
4500
+ if (legacyRoot !== undefined && path.isAbsolute(candidate)) {
4501
+ const resolved = path.resolve(candidate);
4502
+ if (!isPathInside(managedRoot, resolved) && isPathInside(legacyRoot, resolved)) {
4503
+ return resolveProvisionedWorktreePath(candidate, legacyRoot);
4504
+ }
4505
+ }
4506
+ return resolveProvisionedWorktreePath(candidate, managedRoot);
4507
+ }
4492
4508
  function resolveProvisionedWorktreePath(candidate, managedRoot) {
4493
4509
  if (candidate.includes("\u0000") || candidate.trim().length === 0) {
4494
4510
  throw new SessionRegistryError("INVALID_WORKTREE_PATH", `Invalid worktree path: ${candidate}`, {