@gajae-code/coding-agent 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/types/config/model-registry.d.ts +17 -10
  3. package/dist/types/config/models-config-schema.d.ts +37 -0
  4. package/dist/types/config/settings-schema.d.ts +5 -0
  5. package/dist/types/edit/diff.d.ts +16 -0
  6. package/dist/types/edit/modes/replace.d.ts +7 -0
  7. package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
  8. package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
  9. package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
  10. package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
  11. package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
  12. package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
  13. package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
  14. package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
  15. package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
  16. package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
  17. package/dist/types/extensibility/skills.d.ts +9 -1
  18. package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
  19. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
  20. package/dist/types/harness-control-plane/storage.d.ts +7 -0
  21. package/dist/types/lsp/client.d.ts +1 -0
  22. package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
  23. package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
  24. package/dist/types/modes/rpc/rpc-client.d.ts +9 -1
  25. package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
  26. package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
  27. package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
  28. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
  29. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
  30. package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
  31. package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
  32. package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
  33. package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
  34. package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
  35. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
  36. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
  37. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
  38. package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
  39. package/dist/types/modes/theme/theme.d.ts +2 -1
  40. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  41. package/dist/types/sdk.d.ts +7 -0
  42. package/dist/types/session/agent-session.d.ts +10 -0
  43. package/dist/types/session/blob-store.d.ts +17 -0
  44. package/dist/types/session/messages.d.ts +3 -0
  45. package/dist/types/session/session-storage.d.ts +6 -0
  46. package/dist/types/skill-state/active-state.d.ts +13 -0
  47. package/dist/types/thinking.d.ts +3 -2
  48. package/dist/types/tools/index.d.ts +3 -0
  49. package/package.json +9 -7
  50. package/src/cli.ts +14 -0
  51. package/src/commands/harness.ts +192 -7
  52. package/src/commands/ultragoal.ts +1 -21
  53. package/src/config/model-equivalence.ts +1 -1
  54. package/src/config/model-registry.ts +32 -5
  55. package/src/config/models-config-schema.ts +7 -2
  56. package/src/config/settings-schema.ts +4 -1
  57. package/src/discovery/claude-plugins.ts +25 -5
  58. package/src/edit/diff.ts +64 -1
  59. package/src/edit/modes/replace.ts +60 -2
  60. package/src/extensibility/gjc-plugins/activation.ts +87 -0
  61. package/src/extensibility/gjc-plugins/index.ts +9 -0
  62. package/src/extensibility/gjc-plugins/injection.ts +114 -0
  63. package/src/extensibility/gjc-plugins/loader.ts +131 -0
  64. package/src/extensibility/gjc-plugins/paths.ts +66 -0
  65. package/src/extensibility/gjc-plugins/schema.ts +79 -0
  66. package/src/extensibility/gjc-plugins/state.ts +29 -0
  67. package/src/extensibility/gjc-plugins/tools.ts +47 -0
  68. package/src/extensibility/gjc-plugins/types.ts +97 -0
  69. package/src/extensibility/gjc-plugins/validation.ts +76 -0
  70. package/src/extensibility/skills.ts +39 -7
  71. package/src/gjc-runtime/state-runtime.ts +93 -2
  72. package/src/gjc-runtime/state-writer.ts +17 -1
  73. package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
  74. package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
  75. package/src/gjc-runtime/workflow-manifest.ts +2 -2
  76. package/src/harness-control-plane/storage.ts +144 -2
  77. package/src/hashline/hash.ts +23 -0
  78. package/src/hooks/skill-state.ts +2 -0
  79. package/src/internal-urls/docs-index.generated.ts +5 -5
  80. package/src/lsp/client.ts +7 -0
  81. package/src/modes/acp/acp-agent.ts +25 -2
  82. package/src/modes/bridge/bridge-mode.ts +124 -2
  83. package/src/modes/controllers/input-controller.ts +14 -2
  84. package/src/modes/prompt-action-autocomplete.ts +49 -10
  85. package/src/modes/rpc/rpc-client.ts +57 -3
  86. package/src/modes/rpc/rpc-mode.ts +67 -0
  87. package/src/modes/rpc/rpc-types.ts +224 -2
  88. package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
  89. package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
  90. package/src/modes/shared/agent-wire/command-validation.ts +25 -1
  91. package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
  92. package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
  93. package/src/modes/shared/agent-wire/handshake.ts +43 -3
  94. package/src/modes/shared/agent-wire/protocol.ts +7 -0
  95. package/src/modes/shared/agent-wire/responses.ts +2 -2
  96. package/src/modes/shared/agent-wire/scopes.ts +2 -0
  97. package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
  98. package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
  99. package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
  100. package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
  101. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
  102. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
  103. package/src/modes/theme/theme.ts +6 -0
  104. package/src/runtime-mcp/client.ts +7 -4
  105. package/src/runtime-mcp/manager.ts +45 -13
  106. package/src/runtime-mcp/transports/http.ts +40 -14
  107. package/src/runtime-mcp/transports/stdio.ts +11 -10
  108. package/src/sdk.ts +47 -0
  109. package/src/session/agent-session.ts +211 -2
  110. package/src/session/blob-store.ts +84 -0
  111. package/src/session/messages.ts +3 -0
  112. package/src/session/session-manager.ts +390 -33
  113. package/src/session/session-storage.ts +26 -0
  114. package/src/setup/provider-onboarding.ts +2 -2
  115. package/src/skill-state/active-state.ts +89 -1
  116. package/src/task/discovery.ts +7 -1
  117. package/src/task/executor.ts +16 -2
  118. package/src/thinking.ts +8 -2
  119. package/src/tools/ask.ts +39 -9
  120. package/src/tools/index.ts +3 -0
  121. package/src/tools/skill.ts +15 -3
  122. package/src/utils/edit-mode.ts +1 -1
@@ -1,18 +1,12 @@
1
1
  import * as crypto from "node:crypto";
2
2
  import * as path from "node:path";
3
- import { syncSkillActiveState, type WorkflowHudSummary } from "../skill-state/active-state";
3
+ import type { WorkflowHudSummary } from "../skill-state/active-state";
4
4
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
5
- import { WORKFLOW_STATE_VERSION, workflowStateStoragePath } from "../skill-state/workflow-state-contract";
6
5
  import { renderCliWriteReceipt } from "./cli-write-receipt";
7
6
  import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
8
7
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
9
- import {
10
- appendJsonl,
11
- readExistingStateForMutation,
12
- writeArtifact,
13
- writeJsonAtomic,
14
- writeWorkflowEnvelopeAtomic,
15
- } from "./state-writer";
8
+ import { reconcileWorkflowSkillState } from "./state-runtime";
9
+ import { appendJsonl, writeArtifact, writeJsonAtomic } from "./state-writer";
16
10
 
17
11
  export type UltragoalGjcGoalMode = "aggregate" | "per-story";
18
12
  export type UltragoalGoalStatus =
@@ -481,93 +475,6 @@ export function buildUltragoalHudSummary(
481
475
  updatedAt: new Date().toISOString(),
482
476
  });
483
477
  }
484
- function currentSessionId(): string | undefined {
485
- const sessionId = process.env.GJC_SESSION_ID?.trim();
486
- return sessionId || undefined;
487
- }
488
-
489
- function ultragoalModeStateFromSummary(
490
- summary: UltragoalStatusSummary,
491
- latestLedger: UltragoalLedgerEvent | undefined,
492
- existing: Record<string, unknown> | undefined,
493
- sessionId: string | undefined,
494
- ): Record<string, unknown> {
495
- const updatedAt = new Date().toISOString();
496
- return {
497
- ...(existing ?? {}),
498
- skill: "ultragoal",
499
- version: WORKFLOW_STATE_VERSION,
500
- active: summary.exists && summary.status !== "complete",
501
- current_phase: summary.status,
502
- status: summary.status,
503
- active_goal_id: summary.currentGoal?.id,
504
- counts: summary.counts,
505
- brief_path: summary.paths.briefPath,
506
- ledger_path: summary.paths.ledgerPath,
507
- goals_path: summary.paths.goalsPath,
508
- latest_ledger_event: latestLedger?.event,
509
- latest_ledger_event_id: latestLedger?.eventId,
510
- updated_at: updatedAt,
511
- ...(sessionId ? { session_id: sessionId } : {}),
512
- };
513
- }
514
-
515
- export async function syncUltragoalWorkflowState(cwd: string): Promise<void> {
516
- const summary = await getUltragoalStatus(cwd);
517
- const ledger = await readUltragoalLedger(cwd);
518
- const latestLedger = ledger.at(-1);
519
- const sessionId = currentSessionId();
520
- const modeStateActive = summary.exists && summary.status !== "complete";
521
- const syncModeState = async (targetSessionId: string | undefined): Promise<void> => {
522
- const statePath = workflowStateStoragePath(cwd, "ultragoal", targetSessionId);
523
- const existing = await readExistingStateForMutation(statePath);
524
- if (existing.kind === "corrupt" && modeStateActive)
525
- throw new Error(`Cannot sync corrupt ultragoal mode-state: ${existing.error}`);
526
- const existingValue = existing.kind === "valid" ? existing.value : undefined;
527
- await writeWorkflowEnvelopeAtomic(
528
- statePath,
529
- ultragoalModeStateFromSummary(summary, latestLedger, existingValue, targetSessionId),
530
- {
531
- cwd,
532
- receipt: {
533
- cwd,
534
- skill: "ultragoal",
535
- owner: "gjc-runtime",
536
- command: "gjc ultragoal sync",
537
- sessionId: targetSessionId,
538
- },
539
- audit: {
540
- category: "state",
541
- verb: "sync",
542
- owner: "gjc-runtime",
543
- skill: "ultragoal",
544
- fromPhase: typeof existingValue?.current_phase === "string" ? existingValue.current_phase : undefined,
545
- toPhase: summary.status,
546
- },
547
- },
548
- );
549
- };
550
- await syncModeState(undefined);
551
- if (sessionId) await syncModeState(sessionId);
552
- await syncSkillActiveState({
553
- cwd,
554
- skill: "ultragoal",
555
- active: summary.exists && summary.status !== "complete",
556
- phase: summary.status,
557
- sessionId: currentSessionId(),
558
- hud: buildUltragoalHudSummary(summary, latestLedger),
559
- source: "gjc-ultragoal",
560
- });
561
- }
562
-
563
- async function syncUltragoalWorkflowStateBestEffort(cwd: string): Promise<void> {
564
- try {
565
- await syncUltragoalWorkflowState(cwd);
566
- } catch {
567
- // HUD and mode-state sync are best-effort and must not change command semantics.
568
- }
569
- }
570
-
571
478
  function clampTitle(title: string): string {
572
479
  return title.length > 80 ? `${title.slice(0, 77)}...` : title;
573
480
  }
@@ -1400,22 +1307,18 @@ function renderCompleteHandoff(
1400
1307
  ].join("\n");
1401
1308
  }
1402
1309
 
1403
- export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
1310
+ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<UltragoalCommandResult> {
1404
1311
  try {
1405
1312
  const command = commandName(args);
1406
1313
  const json = hasFlag(args, "--json");
1407
- let result: UltragoalCommandResult;
1408
1314
  switch (command) {
1409
1315
  case "status":
1410
- await syncUltragoalWorkflowStateBestEffort(cwd);
1411
- result = { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd), json) };
1412
- break;
1316
+ return { status: 0, stdout: renderStatus(await getUltragoalStatus(cwd), json) };
1413
1317
  case "create":
1414
1318
  case "create-goals": {
1415
1319
  const mode = flagValue(args, "--gjc-goal-mode") === "per-story" ? "per-story" : "aggregate";
1416
1320
  const plan = await createUltragoalPlan({ cwd, brief: await readBrief(cwd, args), gjcGoalMode: mode });
1417
- await syncUltragoalWorkflowStateBestEffort(cwd);
1418
- result = {
1321
+ return {
1419
1322
  status: 0,
1420
1323
  createdPlan: true,
1421
1324
  stdout: json
@@ -1427,17 +1330,16 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1427
1330
  })
1428
1331
  : `Created ultragoal plan with ${plan.goals.length} goal${plan.goals.length === 1 ? "" : "s"} at ${getUltragoalPaths(cwd).goalsPath}.\n`,
1429
1332
  };
1430
- break;
1431
1333
  }
1432
- case "complete-goals": {
1433
- const handoff = await startNextUltragoalGoal({ cwd, retryFailed: hasFlag(args, "--retry-failed") });
1434
- await syncUltragoalWorkflowStateBestEffort(cwd);
1435
- result = {
1334
+ case "complete-goals":
1335
+ return {
1436
1336
  status: 0,
1437
- stdout: renderCompleteHandoff(handoff, json, cwd),
1337
+ stdout: renderCompleteHandoff(
1338
+ await startNextUltragoalGoal({ cwd, retryFailed: hasFlag(args, "--retry-failed") }),
1339
+ json,
1340
+ cwd,
1341
+ ),
1438
1342
  };
1439
- break;
1440
- }
1441
1343
  case "checkpoint": {
1442
1344
  const goalId = flagValue(args, "--goal-id") ?? "";
1443
1345
  const status = parseGoalStatus(flagValue(args, "--status"));
@@ -1450,9 +1352,8 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1450
1352
  gjcGoalJson: flagValue(args, "--gjc-goal-json"),
1451
1353
  qualityGateJson: flagValue(args, "--quality-gate-json"),
1452
1354
  });
1453
- await syncUltragoalWorkflowStateBestEffort(cwd);
1454
1355
  const goal = plan.goals.find(item => item.id === goalId);
1455
- result = {
1356
+ return {
1456
1357
  status: 0,
1457
1358
  stdout: json
1458
1359
  ? renderCliWriteReceipt({
@@ -1465,7 +1366,6 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1465
1366
  })
1466
1367
  : `ultragoal checkpoint goal-id=${goalId} status=${status}\n`,
1467
1368
  };
1468
- break;
1469
1369
  }
1470
1370
  case "steer": {
1471
1371
  const kind = flagValue(args, "--kind");
@@ -1477,9 +1377,8 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1477
1377
  evidence: flagValue(args, "--evidence") ?? "",
1478
1378
  rationale: flagValue(args, "--rationale") ?? "",
1479
1379
  });
1480
- await syncUltragoalWorkflowStateBestEffort(cwd);
1481
1380
  const goal = plan.goals.at(-1);
1482
- result = {
1381
+ return {
1483
1382
  status: 0,
1484
1383
  stdout: json
1485
1384
  ? renderCliWriteReceipt({
@@ -1490,7 +1389,6 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1490
1389
  })
1491
1390
  : "Accepted add_subgoal steering.\n",
1492
1391
  };
1493
- break;
1494
1392
  }
1495
1393
  case "record-review-blockers": {
1496
1394
  const plan = await recordUltragoalReviewBlockers({
@@ -1501,21 +1399,78 @@ export async function runNativeUltragoalCommand(args: string[], cwd = process.cw
1501
1399
  evidence: flagValue(args, "--evidence") ?? "",
1502
1400
  gjcGoalJson: flagValue(args, "--gjc-goal-json"),
1503
1401
  });
1504
- await syncUltragoalWorkflowStateBestEffort(cwd);
1505
1402
  const goal = plan.goals.at(-1);
1506
- result = {
1403
+ return {
1507
1404
  status: 0,
1508
1405
  stdout: json
1509
1406
  ? renderCliWriteReceipt({ ok: true, goal_id: goal?.id, goals_path: getUltragoalPaths(cwd).goalsPath })
1510
1407
  : "Recorded review blockers.\n",
1511
1408
  };
1512
- break;
1513
1409
  }
1514
1410
  default:
1515
1411
  return { status: 1, stderr: `Unknown gjc ultragoal command: ${command}\n` };
1516
1412
  }
1517
- return result;
1518
1413
  } catch (error) {
1519
1414
  return { status: 1, stderr: `${error instanceof Error ? error.message : String(error)}\n` };
1520
1415
  }
1521
1416
  }
1417
+
1418
+ const RECONCILE_COMMANDS = new Set([
1419
+ "status",
1420
+ "create",
1421
+ "create-goals",
1422
+ "complete-goals",
1423
+ "checkpoint",
1424
+ "steer",
1425
+ "record-review-blockers",
1426
+ ]);
1427
+
1428
+ /**
1429
+ * Derive a workflow-state payload from the ultragoal plan/ledger and reconcile the
1430
+ * ultragoal mode-state + active-state/HUD so `gjc state ultragoal read`, the
1431
+ * skill-tool chain guard, and the HUD chip mirror the plan/ledger. Session scope
1432
+ * follows `gjc state` (`GJC_SESSION_ID`). This is a derived repair: it never changes
1433
+ * the triggering command's status/stdout, but a failure is surfaced (stderr + a
1434
+ * `reconcile_failed` ledger audit event) rather than silently swallowed. `status` is
1435
+ * therefore a read PLUS a derived repair; it never mutates goals.json/ledger.jsonl
1436
+ * beyond that reconcile-failure audit event.
1437
+ */
1438
+ async function reconcileUltragoalState(cwd: string): Promise<void> {
1439
+ const sessionId = process.env.GJC_SESSION_ID?.trim() || undefined;
1440
+ try {
1441
+ const summary = await getUltragoalStatus(cwd);
1442
+ const status = summary.status;
1443
+ const active = summary.exists && status !== "complete";
1444
+ const payload: Record<string, unknown> = {
1445
+ skill: "ultragoal",
1446
+ status,
1447
+ current_phase: status,
1448
+ active,
1449
+ goals: summary.goals.map(goal => ({ id: goal.id, title: goal.title, status: goal.status })),
1450
+ counts: summary.counts,
1451
+ active_goal_id: summary.currentGoal?.id ?? null,
1452
+ ledger_path: summary.paths.ledgerPath,
1453
+ brief_path: summary.paths.briefPath,
1454
+ goals_path: summary.paths.goalsPath,
1455
+ };
1456
+ if (summary.gjcObjective) payload.gjc_objective = summary.gjcObjective;
1457
+ await reconcileWorkflowSkillState({ cwd, mode: "ultragoal", sessionId, active, phase: status, payload });
1458
+ } catch (error) {
1459
+ const message = error instanceof Error ? error.message : String(error);
1460
+ process.stderr.write(`ultragoal state reconciliation failed: ${message}\n`);
1461
+ try {
1462
+ await appendLedger(cwd, { type: "reconcile_failed", error: message });
1463
+ } catch {
1464
+ // Best-effort audit; never let a secondary failure change command semantics.
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ export async function runNativeUltragoalCommand(args: string[], cwd = process.cwd()): Promise<UltragoalCommandResult> {
1470
+ const command = commandName(args);
1471
+ const result = await dispatchUltragoalCommand(args, cwd);
1472
+ if (result.status === 0 && RECONCILE_COMMANDS.has(command)) {
1473
+ await reconcileUltragoalState(cwd);
1474
+ }
1475
+ return result;
1476
+ }
@@ -1171,6 +1171,10 @@
1171
1171
  ],
1172
1172
  "skill": "ultragoal",
1173
1173
  "states": [
1174
+ {
1175
+ "id": "missing",
1176
+ "terminal": true
1177
+ },
1174
1178
  {
1175
1179
  "id": "goal-planning",
1176
1180
  "initial": true
@@ -1198,6 +1202,7 @@
1198
1202
  }
1199
1203
  ],
1200
1204
  "terminalStates": [
1205
+ "missing",
1201
1206
  "failed",
1202
1207
  "complete",
1203
1208
  "handoff"
@@ -212,8 +212,8 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
212
212
  }),
213
213
  ultragoal: manifest({
214
214
  skill: "ultragoal",
215
- states: ["goal-planning", "pending", "active", "blocked", "failed", "complete", "handoff"],
216
- terminalStates: ["failed", "complete", "handoff"],
215
+ states: ["missing", "goal-planning", "pending", "active", "blocked", "failed", "complete", "handoff"],
216
+ terminalStates: ["missing", "failed", "complete", "handoff"],
217
217
  transitions: [
218
218
  { from: "goal-planning", to: "pending", verb: "create-goals" },
219
219
  { from: "pending", to: "active", verb: "complete-goals" },
@@ -20,6 +20,85 @@ import * as os from "node:os";
20
20
  import * as path from "node:path";
21
21
  import type { EventEnvelope, ReceiptFamily, SessionState } from "./types";
22
22
 
23
+ interface HarnessRootRegistryEntry {
24
+ root: string;
25
+ updatedAt: string;
26
+ }
27
+
28
+ interface HarnessRootRegistry {
29
+ sessionId: string;
30
+ roots: HarnessRootRegistryEntry[];
31
+ }
32
+
33
+ interface ResolveHarnessSessionRootOptions {
34
+ expectedWorkspace?: string;
35
+ }
36
+
37
+ export function canonicalWorkspacePath(workspace: string): string {
38
+ return path.resolve(workspace);
39
+ }
40
+
41
+ function samePath(left: string, right: string): boolean {
42
+ return canonicalWorkspacePath(left) === canonicalWorkspacePath(right);
43
+ }
44
+
45
+ async function ensurePrivateDir(dir: string): Promise<void> {
46
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
47
+ await fs.chmod(dir, 0o700);
48
+ }
49
+
50
+ function ensurePrivateDirSync(dir: string): void {
51
+ fsSync.mkdirSync(dir, { recursive: true, mode: 0o700 });
52
+ fsSync.chmodSync(dir, 0o700);
53
+ }
54
+
55
+ function sessionMatchesWorkspace(state: SessionState, expectedWorkspace: string): boolean {
56
+ return samePath(state.handle.workspace, expectedWorkspace);
57
+ }
58
+
59
+ function harnessRootRegistryDir(env: NodeJS.ProcessEnv = process.env): string {
60
+ const override = env.GJC_HARNESS_ROOT_REGISTRY_DIR?.trim();
61
+ if (override) return path.resolve(override);
62
+ return path.join(os.tmpdir(), `gjch${process.getuid?.() ?? "u"}`, "harness-roots");
63
+ }
64
+
65
+ function harnessRootRegistryPath(sessionId: string, env: NodeJS.ProcessEnv = process.env): string {
66
+ assertSafeSessionId(sessionId);
67
+ return path.join(harnessRootRegistryDir(env), `${sessionId}.json`);
68
+ }
69
+
70
+ async function readHarnessRootRegistry(
71
+ sessionId: string,
72
+ env: NodeJS.ProcessEnv = process.env,
73
+ ): Promise<HarnessRootRegistry> {
74
+ const file = harnessRootRegistryPath(sessionId, env);
75
+ try {
76
+ const raw = await fs.readFile(file, "utf8");
77
+ const parsed = JSON.parse(raw) as HarnessRootRegistry;
78
+ if (parsed.sessionId === sessionId && Array.isArray(parsed.roots)) return parsed;
79
+ } catch (error) {
80
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
81
+ }
82
+ return { sessionId, roots: [] };
83
+ }
84
+
85
+ async function writeJsonAtomicPrivate(file: string, value: unknown): Promise<void> {
86
+ await ensurePrivateDir(path.dirname(file));
87
+ const tmp = `${file}.tmp-${randomBytes(4).toString("hex")}`;
88
+ await fs.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
89
+ await fs.rename(tmp, file);
90
+ await fs.chmod(file, 0o600);
91
+ }
92
+
93
+ async function writeHarnessRootRegistry(
94
+ registry: HarnessRootRegistry,
95
+ env: NodeJS.ProcessEnv = process.env,
96
+ ): Promise<void> {
97
+ const dir = harnessRootRegistryDir(env);
98
+ await ensurePrivateDir(dir);
99
+ const file = harnessRootRegistryPath(registry.sessionId, env);
100
+ await writeJsonAtomicPrivate(file, registry);
101
+ }
23
102
  const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
24
103
  export const MAX_UNIX_SOCKET_PATH_BYTES = 100;
25
104
 
@@ -36,7 +115,7 @@ function socketBase(env: NodeJS.ProcessEnv, allowOverride: boolean): { base: str
36
115
 
37
116
  function socketPathForBase(root: string, sessionId: string, base: string): string {
38
117
  const digest = createHash("sha256").update(`${root}\0${sessionId}`).digest("hex");
39
- fsSync.mkdirSync(base, { recursive: true });
118
+ ensurePrivateDirSync(base);
40
119
  for (const len of [16, 24, 32, 48, 64]) {
41
120
  const stem = `c-${digest.slice(0, len)}`;
42
121
  const metadataPath = path.join(base, `${stem}.json`);
@@ -46,7 +125,10 @@ function socketPathForBase(root: string, sessionId: string, base: string): strin
46
125
  if (existing.root === root && existing.sessionId === sessionId) return path.join(base, `${stem}.sock`);
47
126
  } catch (error) {
48
127
  if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
49
- fsSync.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
128
+ fsSync.writeFileSync(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, {
129
+ encoding: "utf8",
130
+ mode: 0o600,
131
+ });
50
132
  return path.join(base, `${stem}.sock`);
51
133
  }
52
134
  }
@@ -149,6 +231,66 @@ async function readJson<T>(file: string): Promise<T | null> {
149
231
  export async function readSessionState(root: string, sessionId: string): Promise<SessionState | null> {
150
232
  return readJson<SessionState>(sessionPaths(root, sessionId).state);
151
233
  }
234
+ export async function rememberHarnessSessionRoot(
235
+ root: string,
236
+ sessionId: string,
237
+ env: NodeJS.ProcessEnv = process.env,
238
+ ): Promise<void> {
239
+ assertSafeSessionId(sessionId);
240
+ const resolvedRoot = path.resolve(root);
241
+ const registry = await readHarnessRootRegistry(sessionId, env);
242
+ const now = new Date().toISOString();
243
+ registry.roots = [
244
+ { root: resolvedRoot, updatedAt: now },
245
+ ...registry.roots.filter(entry => path.resolve(entry.root) !== resolvedRoot),
246
+ ].slice(0, 8);
247
+ await writeHarnessRootRegistry(registry, env);
248
+ }
249
+
250
+ export async function resolveHarnessSessionRoot(
251
+ root: string,
252
+ sessionId: string,
253
+ env: NodeJS.ProcessEnv = process.env,
254
+ options: ResolveHarnessSessionRootOptions = {},
255
+ ): Promise<string> {
256
+ assertSafeSessionId(sessionId);
257
+ const resolvedRoot = path.resolve(root);
258
+ const candidates: { root: string; state: SessionState }[] = [];
259
+ const seenRoots = new Set<string>();
260
+ const addCandidate = async (candidateRoot: string): Promise<void> => {
261
+ const candidate = path.resolve(candidateRoot);
262
+ if (seenRoots.has(candidate)) return;
263
+ seenRoots.add(candidate);
264
+ const state = await readSessionState(candidate, sessionId);
265
+ if (state !== null) candidates.push({ root: candidate, state });
266
+ };
267
+
268
+ await addCandidate(resolvedRoot);
269
+ const registry = await readHarnessRootRegistry(sessionId, env);
270
+ for (const entry of registry.roots) await addCandidate(entry.root);
271
+
272
+ if (!options.expectedWorkspace) {
273
+ if (candidates.some(candidate => candidate.root === resolvedRoot)) return resolvedRoot;
274
+ if (candidates.length === 1) return candidates[0].root;
275
+ if (candidates.length > 1) {
276
+ throw new StorageError(`ambiguous_harness_session_root:${sessionId}`, "ambiguous_harness_session_root");
277
+ }
278
+ return resolvedRoot;
279
+ }
280
+
281
+ const expectedWorkspace = canonicalWorkspacePath(options.expectedWorkspace);
282
+ const matchingCandidates = candidates.filter(candidate =>
283
+ sessionMatchesWorkspace(candidate.state, expectedWorkspace),
284
+ );
285
+ if (matchingCandidates.length === 1) return matchingCandidates[0].root;
286
+ if (matchingCandidates.length > 1) {
287
+ throw new StorageError(`ambiguous_harness_session_root:${sessionId}`, "ambiguous_harness_session_root");
288
+ }
289
+ if (candidates.length > 0) {
290
+ throw new StorageError(`session_workspace_mismatch:${sessionId}`, "session_workspace_mismatch");
291
+ }
292
+ return resolvedRoot;
293
+ }
152
294
 
153
295
  export async function writeSessionState(root: string, state: SessionState): Promise<void> {
154
296
  const paths = sessionPaths(root, state.sessionId);
@@ -5,6 +5,20 @@
5
5
 
6
6
  import bigrams from "./bigrams.json" with { type: "json" };
7
7
 
8
+ // Optional native acceleration for formatHashLines. Loaded WITHOUT throwing at
9
+ // module evaluation so this core module (and its re-exported helpers) stays
10
+ // usable and falls back to the TS loop if the native addon is unavailable.
11
+ let formatHashLinesNative: ((text: string, startLine?: number) => string) | undefined;
12
+ void import("../../../natives/native/index.js")
13
+ .then(mod => {
14
+ if (typeof mod.h06FormatHashLines === "function") {
15
+ formatHashLinesNative = mod.h06FormatHashLines;
16
+ }
17
+ })
18
+ .catch(() => {
19
+ // Native unavailable; formatHashLines uses the TS loop.
20
+ });
21
+
8
22
  /**
9
23
  * 647 single-token BPE bigrams for hashline anchors. Every entry tokenizes as
10
24
  * exactly one token in modern BPE vocabularies (cl100k / o200k / Anthropic model family),
@@ -168,6 +182,15 @@ export function formatHashLine(lineNumber: number, line: string): string {
168
182
  * ```
169
183
  */
170
184
  export function formatHashLines(text: string, startLine = 1): string {
185
+ // Native path only for the supported startLine domain (non-negative integer);
186
+ // other values fall through to JS numeric semantics in the TS loop.
187
+ if (formatHashLinesNative && Number.isInteger(startLine) && startLine >= 0) {
188
+ try {
189
+ return formatHashLinesNative(text, startLine);
190
+ } catch {
191
+ // Native hashline formatting is an optimization only; preserve the TS contract.
192
+ }
193
+ }
171
194
  const lines = text.split("\n");
172
195
  return lines.map((line, i) => formatHashLine(startLine + i, line)).join("\n");
173
196
  }
@@ -382,6 +382,8 @@ async function seedSkillActivationState(
382
382
  return state;
383
383
  }
384
384
 
385
+ // Fallback for native-hook prompts when SkillPromptDetails.subskillActivation is absent;
386
+ // real /skill dispatch paths resolve sub-skill activation before prompt construction.
385
387
  export async function recordSkillActivation(input: RecordSkillActivationInput): Promise<SkillActiveState | null> {
386
388
  const match = detectPrimarySkillKeyword(input.text);
387
389
  if (!match) return null;