@gajae-code/coding-agent 0.7.9 → 0.7.10

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 (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +2 -0
  3. package/dist/types/config/settings-schema.d.ts +10 -1
  4. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  5. package/dist/types/gjc-runtime/launch-tmux.d.ts +7 -0
  6. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -0
  7. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
  8. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  9. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +20 -6
  10. package/dist/types/notifications/html-format.d.ts +2 -0
  11. package/dist/types/notifications/recent-activity.d.ts +5 -1
  12. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  13. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  14. package/dist/types/notifications/threaded-render.d.ts +1 -0
  15. package/dist/types/tools/ask.d.ts +2 -0
  16. package/dist/types/utils/cmux-workspace.d.ts +23 -0
  17. package/dist/types/web/search/index.d.ts +1 -1
  18. package/dist/types/web/search/provider.d.ts +5 -0
  19. package/dist/types/web/search/render.d.ts +1 -0
  20. package/package.json +7 -7
  21. package/src/cli/web-search-cli.ts +3 -2
  22. package/src/config/settings-schema.ts +11 -1
  23. package/src/coordinator-mcp/server.ts +1 -1
  24. package/src/defaults/gjc/skills/deep-interview/SKILL.md +26 -2
  25. package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
  26. package/src/extensibility/extensions/types.ts +11 -0
  27. package/src/gjc-runtime/launch-tmux.ts +63 -2
  28. package/src/gjc-runtime/ultragoal-guard.ts +139 -19
  29. package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
  30. package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
  31. package/src/gjc-runtime/workflow-manifest.ts +0 -1
  32. package/src/hooks/skill-state.ts +33 -73
  33. package/src/internal-urls/docs-index.generated.ts +2 -1
  34. package/src/modes/components/hook-selector.ts +21 -3
  35. package/src/modes/components/settings-selector.ts +47 -3
  36. package/src/modes/components/status-line/presets.ts +11 -0
  37. package/src/modes/controllers/extension-ui-controller.ts +18 -2
  38. package/src/modes/shared/agent-wire/deep-interview-gate.ts +53 -9
  39. package/src/notifications/html-format.ts +75 -1
  40. package/src/notifications/index.ts +12 -1
  41. package/src/notifications/recent-activity.ts +32 -8
  42. package/src/notifications/telegram-daemon-control.ts +9 -0
  43. package/src/notifications/telegram-daemon.ts +67 -24
  44. package/src/notifications/telegram-reference.ts +36 -14
  45. package/src/notifications/threaded-render.ts +14 -6
  46. package/src/prompts/system/system-prompt.md +5 -1
  47. package/src/sdk.ts +3 -5
  48. package/src/tools/ask.ts +136 -16
  49. package/src/utils/cmux-workspace.ts +106 -0
  50. package/src/utils/title-generator.ts +2 -0
  51. package/src/web/search/index.ts +22 -10
  52. package/src/web/search/provider.ts +15 -0
  53. package/src/web/search/render.ts +3 -0
@@ -2,12 +2,10 @@ import * as crypto from "node:crypto";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { inflateSync } from "node:zlib";
5
- import { normalizeGoal } from "../goals/state";
6
- import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
7
5
  import type { WorkflowHudSummary } from "../skill-state/active-state";
8
6
  import { buildUltragoalHudSummary as buildWorkflowUltragoalHudSummary } from "../skill-state/workflow-hud";
9
7
  import { renderCliWriteReceipt } from "./cli-write-receipt";
10
- import { DEFAULT_ULTRAGOAL_OBJECTIVE, GJC_SESSION_FILE_ENV } from "./goal-mode-request";
8
+ import { DEFAULT_ULTRAGOAL_OBJECTIVE } from "./goal-mode-request";
11
9
  import { gjcRoot, sessionUltragoalDir } from "./session-layout";
12
10
  import { resolveGjcSessionForRead, resolveGjcSessionForWrite, writeSessionActivityMarker } from "./session-resolution";
13
11
  import { renderUltragoalStatusMarkdown } from "./state-renderer";
@@ -68,7 +66,6 @@ export interface UltragoalCompletionVerification {
68
66
  gjcGoalMode: UltragoalGjcGoalMode;
69
67
  gjcObjective: string;
70
68
  qualityGateHash: string;
71
- gjcGoalSnapshotHash: string;
72
69
  planGeneration: string;
73
70
  basis: {
74
71
  planHashBeforeCheckpoint: string;
@@ -174,10 +171,9 @@ const ACCEPTED_PROOF_STATUSES = new Set([COVERED_STATUS, "passed", "verified"]);
174
171
  const MIN_SUBSTANTIVE_EVIDENCE_WORDS = 5;
175
172
  const MIN_SUBSTANTIVE_EVIDENCE_CHARS = 32;
176
173
 
177
- const GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS = 10 * 60 * 1000;
178
- const GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS = 60 * 1000;
179
-
180
174
  const SCHEDULABLE_STATUSES = new Set<UltragoalGoalStatus>(["pending", "active", "failed"]);
175
+ const COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES = new Set<UltragoalGoalStatus>(["active", "failed"]);
176
+
181
177
  const NATIVE_STEERING_KINDS = [
182
178
  "add_subgoal",
183
179
  "split_subgoal",
@@ -570,7 +566,6 @@ function buildCompletionReceipt(input: {
570
566
  receiptKind: UltragoalReceiptKind;
571
567
  beforeStatus: UltragoalGoalStatus;
572
568
  qualityGateJson: JsonObject;
573
- gjcGoalJson: JsonObject;
574
569
  now: string;
575
570
  checkpointLedgerEventId: string;
576
571
  }): UltragoalCompletionVerification {
@@ -593,7 +588,6 @@ function buildCompletionReceipt(input: {
593
588
  gjcGoalMode: input.plan.gjcGoalMode,
594
589
  gjcObjective: input.plan.gjcObjective,
595
590
  qualityGateHash: hashStructuredValue(input.qualityGateJson),
596
- gjcGoalSnapshotHash: hashStructuredValue(input.gjcGoalJson),
597
591
  planGeneration: generation.planGeneration,
598
592
  basis: generation.basis,
599
593
  checkpointLedgerEventId: input.checkpointLedgerEventId,
@@ -2470,93 +2464,24 @@ async function readRequiredCompletionQualityGate(
2470
2464
  return gate;
2471
2465
  }
2472
2466
 
2473
- function snapshotUpdatedAtMilliseconds(value: unknown): number | null {
2474
- if (typeof value === "number" && Number.isFinite(value)) return value;
2475
- if (typeof value !== "string" || value.trim().length === 0) return null;
2476
- const trimmed = value.trim();
2477
- if (/^\d+$/.test(trimmed)) {
2478
- const parsed = Number.parseInt(trimmed, 10);
2479
- return Number.isFinite(parsed) ? parsed : null;
2480
- }
2481
- const parsed = Date.parse(trimmed);
2482
- return Number.isFinite(parsed) ? parsed : null;
2483
- }
2484
-
2485
- function singleSessionLeafId(entries: readonly SessionEntry[]): string | undefined {
2486
- if (entries.length === 0) return undefined;
2487
- const parentIds = new Set(
2488
- entries.map(entry => entry.parentId).filter((parentId): parentId is string => typeof parentId === "string"),
2489
- );
2490
- const leafIds = entries.map(entry => entry.id).filter(id => !parentIds.has(id));
2491
- return leafIds.length === 1 ? leafIds[0] : undefined;
2492
- }
2493
-
2494
- async function readCurrentSessionGjcGoalSnapshot(): Promise<unknown | undefined> {
2495
- const sessionFile = process.env[GJC_SESSION_FILE_ENV]?.trim();
2496
- if (!sessionFile) return undefined;
2497
- const fileEntries = await loadEntriesFromFile(sessionFile);
2498
- const entries = fileEntries.filter((entry): entry is SessionEntry => entry.type !== "session");
2499
- const leafId = singleSessionLeafId(entries);
2500
- if (!leafId) return undefined;
2501
- const context = buildSessionContext(entries, leafId);
2502
- if (context.mode !== "goal" && context.mode !== "goal_paused") return undefined;
2503
- const goal = normalizeGoal(context.modeData?.goal);
2504
- return goal ? { goal } : undefined;
2505
- }
2506
- async function readGjcGoalSnapshot(input: {
2507
- cwd: string;
2508
- value: string | undefined;
2509
- plan: UltragoalPlan;
2510
- goal?: UltragoalGoal;
2511
- required: boolean;
2512
- errorPrefix: string;
2513
- allowCompletedLegacyBlocker?: boolean;
2514
- }): Promise<unknown> {
2515
- const snapshot = input.value?.trim()
2516
- ? await readStructuredValue(input.cwd, input.value)
2517
- : await readCurrentSessionGjcGoalSnapshot();
2518
- if (snapshot === undefined) {
2519
- if (!input.required) return undefined;
2520
- throw new Error(
2521
- `${input.errorPrefix} require an active GJC goal-mode snapshot from the current session or --gjc-goal-json; this is the GJC goal-mode receipt, not the .gjc/ultragoal/goals.json goal record`,
2522
- );
2523
- }
2524
- const snapshotObject = qualityGateObject(snapshot);
2525
- const detailsObject = qualityGateObject(snapshotObject?.details);
2526
- const goalObject = qualityGateObject(snapshotObject?.goal) ?? qualityGateObject(detailsObject?.goal);
2527
- if (!goalObject)
2528
- throw new Error(
2529
- `${input.errorPrefix} require --gjc-goal-json with a goal object from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
2530
- );
2531
- const updatedAt = snapshotUpdatedAtMilliseconds(goalObject.updatedAt);
2532
- if (!updatedAt)
2467
+ function validateCompleteCheckpointTargetGoal(goal: UltragoalGoal): void {
2468
+ if (COMPLETE_CHECKPOINT_ALLOWED_PRE_STATUSES.has(goal.status)) return;
2469
+ if (goal.status === "pending") {
2533
2470
  throw new Error(
2534
- `${input.errorPrefix} require --gjc-goal-json goal.updatedAt as epoch milliseconds or an ISO timestamp from goal({"op":"get"}); pass the active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record`,
2471
+ `Cannot checkpoint ${goal.id} as complete while its durable goals.json status is pending; start the goal before completing it.`,
2535
2472
  );
2536
- const nowMilliseconds = Date.now();
2537
- if (updatedAt < nowMilliseconds - GJC_GOAL_SNAPSHOT_MAX_AGE_MILLISECONDS) {
2538
- throw new Error(`${input.errorPrefix} require a fresh --gjc-goal-json snapshot`);
2539
- }
2540
- if (updatedAt > nowMilliseconds + GJC_GOAL_SNAPSHOT_MAX_FUTURE_SKEW_MILLISECONDS) {
2541
- throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.updatedAt that is not from the future`);
2542
- }
2543
- const objective = typeof goalObject.objective === "string" ? goalObject.objective : "";
2544
- const expectedObjectives = new Set([input.plan.gjcObjective, ...(input.plan.gjcObjectiveAliases ?? [])]);
2545
- if (input.plan.gjcGoalMode === "per-story" && input.goal?.objective) {
2546
- expectedObjectives.add(input.goal.objective);
2547
- }
2548
- if (input.allowCompletedLegacyBlocker && goalObject.status === "complete" && !expectedObjectives.has(objective)) {
2549
- return snapshot;
2550
2473
  }
2551
- if (!expectedObjectives.has(objective)) {
2474
+ if (goal.status === "complete") {
2552
2475
  throw new Error(
2553
- `${input.errorPrefix} require --gjc-goal-json objective to match the active GJC goal-mode objective from goal({"op":"get"}), not the .gjc/ultragoal/goals.json goal ${input.goal?.id ?? "record"}`,
2476
+ `Cannot checkpoint ${goal.id} as complete with different evidence because its durable goals.json status is already complete.`,
2554
2477
  );
2555
2478
  }
2556
- if (goalObject.status !== "active") {
2557
- throw new Error(`${input.errorPrefix} require --gjc-goal-json goal.status to be active`);
2479
+ if (goal.status === "superseded") {
2480
+ throw new Error(`Cannot checkpoint ${goal.id} as complete because its durable goals.json status is superseded.`);
2558
2481
  }
2559
- return snapshot;
2482
+ throw new Error(
2483
+ `Cannot checkpoint ${goal.id} as complete while its durable goals.json status is ${goal.status}; only active or retryable failed goals can be completed.`,
2484
+ );
2560
2485
  }
2561
2486
 
2562
2487
  export async function checkpointUltragoalGoal(input: {
@@ -2564,7 +2489,6 @@ export async function checkpointUltragoalGoal(input: {
2564
2489
  goalId: string;
2565
2490
  status: UltragoalGoalStatus;
2566
2491
  evidence: string;
2567
- gjcGoalJson?: string;
2568
2492
  qualityGateJson?: string;
2569
2493
  }): Promise<UltragoalPlan> {
2570
2494
  const plan = await readUltragoalPlan(input.cwd);
@@ -2593,6 +2517,7 @@ export async function checkpointUltragoalGoal(input: {
2593
2517
  // instead of silently dropping it.
2594
2518
  return plan;
2595
2519
  }
2520
+ if (input.status === "complete") validateCompleteCheckpointTargetGoal(goal);
2596
2521
  const changeSet = input.status === "complete" ? await computeCheckpointChangeSet(input.cwd) : undefined;
2597
2522
  const qualityGateJson =
2598
2523
  input.status === "complete"
@@ -2615,25 +2540,6 @@ export async function checkpointUltragoalGoal(input: {
2615
2540
  }
2616
2541
  }
2617
2542
  const receiptKind = input.status === "complete" ? chooseReceiptKind(plan, goal, input.status) : null;
2618
- const gjcGoalJson =
2619
- input.status === "complete"
2620
- ? await readGjcGoalSnapshot({
2621
- cwd: input.cwd,
2622
- value: input.gjcGoalJson,
2623
- plan,
2624
- goal,
2625
- required: true,
2626
- errorPrefix: "complete checkpoints",
2627
- })
2628
- : await readGjcGoalSnapshot({
2629
- cwd: input.cwd,
2630
- value: input.gjcGoalJson,
2631
- plan,
2632
- goal,
2633
- required: false,
2634
- errorPrefix: `${input.status} checkpoints`,
2635
- allowCompletedLegacyBlocker: input.status === "blocked",
2636
- });
2637
2543
  const pendingCheckpointEventId = crypto.randomUUID();
2638
2544
  if (input.status === "complete" && receiptKind && qualityGateJson && !Array.isArray(qualityGateJson)) {
2639
2545
  goal.completionVerification = buildCompletionReceipt({
@@ -2643,7 +2549,6 @@ export async function checkpointUltragoalGoal(input: {
2643
2549
  receiptKind,
2644
2550
  beforeStatus,
2645
2551
  qualityGateJson: qualityGateJson as JsonObject,
2646
- gjcGoalJson: gjcGoalJson as JsonObject,
2647
2552
  now,
2648
2553
  checkpointLedgerEventId: pendingCheckpointEventId,
2649
2554
  });
@@ -2662,7 +2567,6 @@ export async function checkpointUltragoalGoal(input: {
2662
2567
  goalId: goal.id,
2663
2568
  status: input.status,
2664
2569
  evidence,
2665
- gjcGoalJson,
2666
2570
  qualityGateJson,
2667
2571
  completionVerification: goal.completionVerification,
2668
2572
  });
@@ -2682,7 +2586,6 @@ export async function checkpointAndContinueUltragoalGoal(input: {
2682
2586
  goalId: string;
2683
2587
  status: UltragoalGoalStatus;
2684
2588
  evidence: string;
2685
- gjcGoalJson?: string;
2686
2589
  qualityGateJson?: string;
2687
2590
  advanceNext?: boolean;
2688
2591
  retryFailed?: boolean;
@@ -3091,19 +2994,14 @@ export async function recordUltragoalReviewBlockers(input: {
3091
2994
  title: string;
3092
2995
  objective: string;
3093
2996
  evidence: string;
3094
- gjcGoalJson?: string;
3095
2997
  }): Promise<UltragoalPlan> {
3096
2998
  const objective = input.objective.trim();
3097
2999
  if (!objective) throw new Error("record-review-blockers --objective is required");
3098
- if (!input.gjcGoalJson?.trim()) {
3099
- throw new Error('record-review-blockers require --gjc-goal-json from a fresh active goal({"op":"get"}) snapshot');
3100
- }
3101
3000
  const plan = await checkpointUltragoalGoal({
3102
3001
  cwd: input.cwd,
3103
3002
  goalId: input.goalId,
3104
3003
  status: "review_blocked",
3105
3004
  evidence: input.evidence,
3106
- gjcGoalJson: input.gjcGoalJson,
3107
3005
  });
3108
3006
  const persistedPlan = await readUltragoalPlan(input.cwd);
3109
3007
  if (persistedPlan?.state_revision !== undefined) plan.state_revision = persistedPlan.state_revision;
@@ -3551,7 +3449,6 @@ const FLAGS_WITH_VALUES = new Set([
3551
3449
  "--goal-id",
3552
3450
  "--status",
3553
3451
  "--evidence",
3554
- "--gjc-goal-json",
3555
3452
  "--quality-gate-json",
3556
3453
  "--executor-qa-json",
3557
3454
  "--executor-qa",
@@ -3605,15 +3502,12 @@ function renderUltragoalHelp(args: readonly string[]): string | null {
3605
3502
  " --status=<value> pending|active|complete|failed|blocked|review_blocked|superseded",
3606
3503
  " --evidence=<value> Completion or checkpoint evidence text",
3607
3504
  " --quality-gate-json=<value> JSON string or path for complete checkpoints",
3608
- " --gjc-goal-json=<value> Optional JSON/path override for current goal snapshot; omitted complete checkpoints read current session goal state",
3609
3505
  " --json Output a machine-readable receipt",
3610
3506
  "",
3611
3507
  "COMPLETE CHECKPOINT RECEIPTS",
3612
3508
  " --quality-gate-json must be an object with architectReview, executorQa, and iteration.",
3613
3509
  " executorQa.contractCoverage[] rows require an obligation field; description is not a substitute.",
3614
- " Complete checkpoints use the current session's active GJC goal-mode snapshot when --gjc-goal-json is omitted.",
3615
- " Explicit --gjc-goal-json values must contain an active GJC goal-mode snapshot, not the .gjc/ultragoal/goals.json goal record.",
3616
- " goal.updatedAt may be epoch milliseconds or an ISO timestamp and must be fresh.",
3510
+ " Complete checkpoints validate the target durable goals.json record before writing a receipt.",
3617
3511
  "",
3618
3512
  "EXAMPLES",
3619
3513
  ' $ gjc ultragoal checkpoint --goal-id G001 --status blocked --evidence "waiting on review"',
@@ -3949,7 +3843,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3949
3843
  goalId,
3950
3844
  status,
3951
3845
  evidence,
3952
- gjcGoalJson: flagValue(args, "--gjc-goal-json"),
3953
3846
  qualityGateJson: flagValue(args, "--quality-gate-json"),
3954
3847
  advanceNext: status === "complete",
3955
3848
  });
@@ -3981,7 +3874,6 @@ async function dispatchUltragoalCommand(args: string[], cwd: string): Promise<Ul
3981
3874
  title: flagValue(args, "--title") ?? "Resolve final code-review blockers",
3982
3875
  objective: flagValue(args, "--objective") ?? "",
3983
3876
  evidence: flagValue(args, "--evidence") ?? "",
3984
- gjcGoalJson: flagValue(args, "--gjc-goal-json"),
3985
3877
  });
3986
3878
  const goal = plan.goals.at(-1);
3987
3879
  return {
@@ -1479,14 +1479,6 @@
1479
1479
  "required": true,
1480
1480
  "type": "string"
1481
1481
  },
1482
- {
1483
- "appliesToVerbs": [
1484
- "checkpoint",
1485
- "record-review-blockers"
1486
- ],
1487
- "name": "gjc-goal-json",
1488
- "type": "string"
1489
- },
1490
1482
  {
1491
1483
  "appliesToVerbs": [
1492
1484
  "checkpoint"
@@ -271,7 +271,6 @@ export const WORKFLOW_MANIFEST: Record<CanonicalGjcWorkflowSkill, SkillManifest>
271
271
  required: true,
272
272
  appliesToVerbs: ["checkpoint", "record-review-blockers", "steer", "classify-blocker"],
273
273
  },
274
- { name: "gjc-goal-json", type: "string", appliesToVerbs: ["checkpoint", "record-review-blockers"] },
275
274
  { name: "quality-gate-json", type: "string", appliesToVerbs: ["checkpoint"] },
276
275
  { name: "goal-id", type: "string", appliesToVerbs: ["steer"] },
277
276
  { name: "goal-id", type: "string", appliesToVerbs: ["classify-blocker"] },
@@ -11,8 +11,7 @@ import {
11
11
  writeGuardedJsonAtomic,
12
12
  writeGuardedWorkflowEnvelopeAtomic,
13
13
  } from "../gjc-runtime/state-writer";
14
- import { isUltragoalBypassPrompt, readUltragoalVerificationState } from "../gjc-runtime/ultragoal-guard";
15
- import { getUltragoalRunCompletionState, readUltragoalPlan } from "../gjc-runtime/ultragoal-runtime";
14
+ import { isUltragoalBypassPrompt, verifyUltragoalDurableCompletionState } from "../gjc-runtime/ultragoal-guard";
16
15
  import { buildSessionContext, loadEntriesFromFile, type SessionEntry } from "../session/session-manager";
17
16
  import {
18
17
  readVisibleSkillActiveState as readCanonicalVisibleSkillActiveState,
@@ -565,6 +564,10 @@ function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean
565
564
  return false;
566
565
  }
567
566
 
567
+ function ultragoalDurableCompletionReleasesStop(state: string): boolean {
568
+ return state === "inactive" || state === "active_verified_complete";
569
+ }
570
+
568
571
  /**
569
572
  * Cross-file coherence guard for a mode-state that claims it releases the Stop
570
573
  * block. `modeStateReleasesStop` trusts a single mode-state file; if any writer
@@ -577,18 +580,15 @@ function modeStateReleasesStop(state: ModeState | null, handoffRequired: boolean
577
580
  * cheap and read-only — ultragoal reads the durable plan; skills without an
578
581
  * independent durable source release as before.
579
582
  */
580
- async function detectStaleModeStateRelease(skill: GjcWorkflowSkill, cwd: string): Promise<string | null> {
581
- if (skill === "ultragoal") {
582
- const plan = await readUltragoalPlan(cwd);
583
- if (!plan) return null;
584
- const runState = getUltragoalRunCompletionState(plan);
585
- if (runState.incompleteGoals.length > 0) {
586
- return `the durable Ultragoal plan still has incomplete required goals (${runState.incompleteGoals
587
- .map(goal => goal.id)
588
- .join(", ")}); run \`gjc ultragoal complete-goals\` to continue`;
589
- }
590
- }
591
- return null;
583
+ async function detectStaleModeStateRelease(
584
+ skill: GjcWorkflowSkill,
585
+ cwd: string,
586
+ sessionId?: string | null,
587
+ ): Promise<string | null> {
588
+ if (skill !== "ultragoal") return null;
589
+ const diagnostic = await verifyUltragoalDurableCompletionState({ cwd, sessionId });
590
+ if (ultragoalDurableCompletionReleasesStop(diagnostic.state)) return null;
591
+ return `${diagnostic.message} Run \`gjc ultragoal complete-goals\` to continue, or checkpoint a finished story with \`gjc ultragoal checkpoint --status complete --quality-gate-json <file>\`, before stopping`;
592
592
  }
593
593
 
594
594
  /**
@@ -663,19 +663,6 @@ function stateMatchesContext(state: ModeState, sessionId?: string, threadId?: st
663
663
  return true;
664
664
  }
665
665
 
666
- async function readCurrentGoalObjectiveFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
667
- const trimmed = sessionFile?.trim();
668
- if (!trimmed) return null;
669
- const entries = (await loadEntriesFromFile(trimmed)).filter(
670
- (entry): entry is SessionEntry => entry.type !== "session",
671
- );
672
- const context = buildSessionContext(entries);
673
- const goal = context.modeData?.goal;
674
- if (typeof goal !== "object" || goal === null) return null;
675
- const objective = (goal as { objective?: unknown }).objective;
676
- return typeof objective === "string" && objective.trim().length > 0 ? objective.trim() : null;
677
- }
678
-
679
666
  async function readLatestAssistantTextFromSessionFile(sessionFile: string | undefined): Promise<string | null> {
680
667
  const trimmed = sessionFile?.trim();
681
668
  if (!trimmed) return null;
@@ -730,34 +717,18 @@ export async function buildActiveUltragoalPromptContext(input: UserPromptSubmitS
730
717
  if (!stateMatchesContext(visibleModeState.state, resolvedSessionId, input.threadId)) return null;
731
718
 
732
719
  const phase = String(visibleModeState.state.current_phase ?? "active");
733
- const stateObjective =
734
- typeof visibleModeState.state.objective === "string"
735
- ? visibleModeState.state.objective
736
- : typeof visibleModeState.state.gjcObjective === "string"
737
- ? visibleModeState.state.gjcObjective
738
- : "";
739
- const sessionObjective = await readCurrentGoalObjectiveFromSessionFile(input.sessionFile);
740
720
  const normalizedPrompt = input.prompt?.replace(/\\?"/g, '"');
741
721
  const isBypassPrompt = Boolean(
742
722
  (normalizedPrompt && isUltragoalBypassPrompt(normalizedPrompt)) ||
743
723
  (input.prompt && /goal[\s\S]{0,80}complete/i.test(input.prompt)),
744
724
  );
745
725
  if (isBypassPrompt) {
746
- const objectives = [sessionObjective, stateObjective].filter(
747
- (value): value is string => typeof value === "string" && value.trim().length > 0,
748
- );
749
- if (objectives.length === 0) {
750
- return "BLOCK_ULTRAGOAL_COMPLETION: Active Ultragoal completion is blocked until a current GJC goal objective can be verified. Use durable blocker work or run strict `gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>` before completion.";
751
- }
752
- for (const objective of objectives) {
753
- const diagnostic = await readUltragoalVerificationState({
754
- cwd: input.cwd,
755
- currentGoal: { objective },
756
- });
757
- if (diagnostic.state === "unrelated_goal") continue;
758
- if (!["inactive", "active_verified_complete"].includes(diagnostic.state)) {
759
- return `BLOCK_ULTRAGOAL_COMPLETION: ${diagnostic.message} Use durable blocker work or run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file> --gjc-goal-json <file>\` before completion.`;
760
- }
726
+ const diagnostic = await verifyUltragoalDurableCompletionState({
727
+ cwd: input.cwd,
728
+ sessionId: resolvedSessionId,
729
+ });
730
+ if (!ultragoalDurableCompletionReleasesStop(diagnostic.state)) {
731
+ return `BLOCK_ULTRAGOAL_COMPLETION: ${diagnostic.message} Use durable blocker work or run strict \`gjc ultragoal checkpoint --status complete --quality-gate-json <file>\` before completion.`;
761
732
  }
762
733
  }
763
734
  return `Ultragoal is active (phase: ${phase}; state: ${visibleModeState.statePath}). If the user prompt is a steering request, use \`gjc ultragoal steer\` to add or steer subgoals. Normal prose should not mutate Ultragoal state.`;
@@ -818,7 +789,7 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
818
789
  // authoritative durable state. If a stale/incoherent mode-state would
819
790
  // release while the plan/ledger still shows pending work, block instead
820
791
  // of trusting the single file (see #659).
821
- const staleRelease = await detectStaleModeStateRelease(entry.skill, input.cwd);
792
+ const staleRelease = await detectStaleModeStateRelease(entry.skill, input.cwd, resolvedSessionId);
822
793
  if (staleRelease) {
823
794
  const coherenceMessage = `GJC skill "${entry.skill}" mode-state reports it released the Stop block (${modeStatePath(input.cwd, entry.skill, resolvedSessionId)}), but ${staleRelease}. The mode-state is incoherent with authoritative durable state; finish or explicitly clear the pending work before stopping.`;
824
795
  return {
@@ -847,29 +818,18 @@ export async function buildSkillStopOutput(input: StopHookInput): Promise<Record
847
818
  const phase = String(modeState?.current_phase ?? entry.phase ?? skillState.phase ?? "active");
848
819
  const statePath = modeStatePath(input.cwd, entry.skill, resolvedSessionId);
849
820
  if (entry.skill === "ultragoal") {
850
- const objective =
851
- (await readCurrentGoalObjectiveFromSessionFile(input.sessionFile)) ??
852
- (typeof modeState?.objective === "string"
853
- ? modeState.objective
854
- : typeof modeState?.gjcObjective === "string"
855
- ? modeState.gjcObjective
856
- : "");
857
- if (objective) {
858
- const diagnostic = await readUltragoalVerificationState({
859
- cwd: input.cwd,
860
- currentGoal: { objective },
861
- });
862
- if (diagnostic.state === "active_verified_complete") continue;
863
- if (!["inactive", "unrelated_goal"].includes(diagnostic.state)) {
864
- const ultragoalMessage = `GJC ultragoal verification is blocking stop: ${diagnostic.message} Run strict checkpoint verification or record review blockers before stopping.`;
865
- return {
866
- decision: "block",
867
- reason: ultragoalMessage,
868
- stopReason: `gjc_ultragoal_verification_${diagnostic.state}`,
869
- systemMessage: ultragoalMessage,
870
- };
871
- }
872
- }
821
+ const diagnostic = await verifyUltragoalDurableCompletionState({
822
+ cwd: input.cwd,
823
+ sessionId: resolvedSessionId,
824
+ });
825
+ if (ultragoalDurableCompletionReleasesStop(diagnostic.state)) continue;
826
+ const ultragoalMessage = `GJC ultragoal verification is blocking stop: ${diagnostic.message} Run \`gjc ultragoal checkpoint --status complete --quality-gate-json <file>\` or record review blockers before stopping.`;
827
+ return {
828
+ decision: "block",
829
+ reason: ultragoalMessage,
830
+ stopReason: `gjc_ultragoal_verification_${diagnostic.state}`,
831
+ systemMessage: ultragoalMessage,
832
+ };
873
833
  }
874
834
  const systemMessage = handoffRequired
875
835
  ? buildHandoffForceAskMessage(entry.skill, phase, statePath)
@@ -1,6 +1,6 @@
1
1
  // Auto-generated by scripts/generate-docs-index.ts - DO NOT EDIT
2
2
 
3
- export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["ERRATA-GPT5-HARMONY.md","REBRANDING_PLAN_260525.md","ai-schema-normalize.md","aside-integration.md","auth-broker-gateway.md","bash-tool-runtime.md","blob-artifact-architecture.md","bot-integration.md","brand-assets.md","bridge.md","codebase-overview.md","codegraph-custom-tool.md","compaction.md","composer-codex-parity.md","computer-use/README.md","environment-variables.md","external-control-readiness.md","fs-scan-cache-architecture.md","geobench.md","gjc-dogfood-skill-template.md","gjc-plugins.md","gjc-session-clawhip-routing.md","grok-build-provider-design.md","handoff-generation-pipeline.md","hermes-mcp-bridge.md","hotspot-map-successor.md","keybindings.md","lsp-config.md","memory.md","models.md","multi-vendor-profiles.md","native-ffi-optimization-policy.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-package-split-plan.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","notifications-sdk.md","onboarding-packet.md","onboarding-receipt.md","ooo-bridge-extension-contract.md","openclaw-hermes-rpc-integration.md","perf-profiling-corpus.md","porting-from-pi-mono.md","porting-to-natives.md","provider-streaming-internals.md","python-repl.md","render-mermaid.md","research-plan-ledger.md","resolve-tool-runtime.md","rpc.md","rulebook-matching-pipeline.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","standalone-mcp.md","telegram-onboarding.md","theme.md","tools/ask.md","tools/ast-edit.md","tools/ast-grep.md","tools/bash.md","tools/browser.md","tools/calc.md","tools/checkpoint.md","tools/computer.md","tools/cron.md","tools/debug.md","tools/edit.md","tools/eval.md","tools/find.md","tools/github.md","tools/irc.md","tools/job.md","tools/lsp.md","tools/monitor.md","tools/read.md","tools/recipe.md","tools/render_mermaid.md","tools/resolve.md","tools/rewind.md","tools/search.md","tools/search_tool_bm25.md","tools/ssh.md","tools/task.md","tools/todo_write.md","tools/web_search.md","tools/write.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","ui-design-visual-qa.md"];
3
+ export const EMBEDDED_DOC_FILENAMES: readonly string[] = ["ERRATA-GPT5-HARMONY.md","REBRANDING_PLAN_260525.md","ai-schema-normalize.md","aside-integration.md","auth-broker-gateway.md","bash-tool-runtime.md","blob-artifact-architecture.md","bot-integration.md","brand-assets.md","bridge.md","codebase-overview.md","codegraph-custom-tool.md","compaction.md","composer-codex-parity.md","computer-use/README.md","environment-variables.md","external-control-readiness.md","fs-scan-cache-architecture.md","geobench.md","git-daemon.md","gjc-dogfood-skill-template.md","gjc-plugins.md","gjc-session-clawhip-routing.md","grok-build-provider-design.md","handoff-generation-pipeline.md","hermes-mcp-bridge.md","hotspot-map-successor.md","keybindings.md","lsp-config.md","memory.md","models.md","multi-vendor-profiles.md","native-ffi-optimization-policy.md","natives-addon-loader-runtime.md","natives-architecture.md","natives-binding-contract.md","natives-build-release-debugging.md","natives-media-system-utils.md","natives-package-split-plan.md","natives-rust-task-cancellation.md","natives-shell-pty-process.md","natives-text-search-pipeline.md","non-compaction-retry-policy.md","notebook-tool-runtime.md","notifications-sdk.md","onboarding-packet.md","onboarding-receipt.md","ooo-bridge-extension-contract.md","openclaw-hermes-rpc-integration.md","perf-profiling-corpus.md","porting-from-pi-mono.md","porting-to-natives.md","provider-streaming-internals.md","python-repl.md","render-mermaid.md","research-plan-ledger.md","resolve-tool-runtime.md","rpc.md","rulebook-matching-pipeline.md","sdk.md","secrets.md","session-operations-export-share-fork-resume.md","session-switching-and-recent-listing.md","session-tree-plan.md","session.md","standalone-mcp.md","telegram-onboarding.md","theme.md","tools/ask.md","tools/ast-edit.md","tools/ast-grep.md","tools/bash.md","tools/browser.md","tools/calc.md","tools/checkpoint.md","tools/computer.md","tools/cron.md","tools/debug.md","tools/edit.md","tools/eval.md","tools/find.md","tools/github.md","tools/irc.md","tools/job.md","tools/lsp.md","tools/monitor.md","tools/read.md","tools/recipe.md","tools/render_mermaid.md","tools/resolve.md","tools/rewind.md","tools/search.md","tools/search_tool_bm25.md","tools/ssh.md","tools/task.md","tools/todo_write.md","tools/web_search.md","tools/write.md","tree.md","ttsr-injection-lifecycle.md","tui-runtime-internals.md","ui-design-visual-qa.md"];
4
4
 
5
5
  export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
6
6
  "ERRATA-GPT5-HARMONY.md": "# ERRATA — GPT-5 Harmony-Header Leakage\n\n## 1. The problem\n\nOpenAI frames tool calls in the Harmony chat protocol:\n\n```\n<|start|>assistant<|channel|>commentary to=functions.<NAME><|message|>{ARGS}<|call|>\n```\n\n`<|channel|>commentary to=functions.NAME` is the **routing header** —\ncontrol tokens consumed by the runtime to dispatch the call. These\ntokens never appear as content under normal operation; the runtime\nstrips them.\n\nThe defect: gpt-5 models occasionally emit, **as ordinary content\ninside `{ARGS}`**, the **plain-text shadow** of these routing tokens —\nthe same characters without the `<|…|>` brackets — and continue\nproducing more pseudo-routing structure (channel name, body marker,\nmultilingual spam, fake tool-result framing). The contamination lives\ninside the visible tool argument and is dispatched to the tool as if it\nwere intended content.\n\n**Critical detail.** The actual `<|start|>` / `<|channel|>` /\n`<|message|>` / `<|call|>` special tokens almost never appear in tool\nargs. What leaks is the bracket-less spelling — `analysis to=functions.X\ncode …` — because OpenAI applies a logit mask suppressing the\ncontrol-token IDs inside the args region. The mass that would have gone\nto those special tokens redistributes onto the un-bracketed plain-text\nrepresentation the model also learned. This makes the leak structurally\ninvisible to the routing parser and lands it in the tool input verbatim.\n\nManifestation in tool args (real corpus example):\n\n```\n~ add_function(iso, ctx, ns, \"installSystemChangeObserver\",\n os_install_system_change_observer);】【\"】【analysis to=functions.edit\n code above เงินไทยฟรีuser to=functions.edit code …\n```\n\nThe leading code is real and intended. Everything after the first\nnon-Latin token through the next clean structural boundary is corruption.\n\n---\n\n## 2. Observed statistics & failure modes\n\nSource: `~/.gjc/stats.db` (`ss_tool_calls`, `ss_assistant_msgs`), through\n2026-05-10. 1.05M tool calls scanned.\n\n### 2.1 Rate\n\n| Model | Leaks in tool args | Calls | per million |\n|------------------|-------------------:|--------:|------------:|\n| gpt-5.4 | 37 | 226,957 | 163 |\n| gpt-5.3-openai-code | 17 | 112,243 | 151 |\n| gpt-5.5 | 2 | 80,750 | 25 |\n| gpt-5.2-openai-code | 0 | — | — |\n\nPlus 15 hits in assistant visible text / thinking blobs.\n\n### 2.2 Tool distribution\n\n| Tool | Hits |\n|---------------------|-----:|\n| `edit` | 38 |\n| `eval` | 11 |\n| `report_tool_issue` | 3 |\n| `grep`/`read`/`search`/`yield` | 1 each |\n\nConcentrated in tools with free-form (non-JSON-schema) argument formats.\n\n### 2.3 Leak shape (deterministic)\n\n```\nLEAK ::= JUNK_PREFIX MARKER CHANNEL_BODY (LEAK)?\nMARKER ::= \"to=functions.\" TOOL_NAME\nCHANNEL_BODY ::= \" code \" (SPAM | reasoning_prose | fake_tool_output)*\nJUNK_PREFIX ::= (GLITCH_TOKEN | CHANNEL_WORD | NON_LATIN_RUN | \"}\" | \"】【\")+\n```\n\n**Cascading is common.** Of 96 marker occurrences across 71 contaminated\nrecords, 39 contain ≥2 markers and 7 contain ≥3 — the model emits\nmultiple fake `to=functions.X code …` blocks back-to-back, often with\nfake `code_output\\nCell N:\\n…` framing between them. Once the\nplain-text scaffolding is in the residual stream, the prefix now *looks\nlike* a fresh tool envelope start, so the macro prior over continuations\nkeeps voting for more scaffolding. Self-amplifying.\n\n### 2.4 Glitch tokens\n\nSingle-token identifiers in `o200k_base` whose embeddings appear to be\nnear-init from underrepresentation in post-training. ASCII residue\nimmediately before the marker in the natural corpus:\n\n| Surface string | Single-token | Token ID | Hits in corpus |\n|-------------------|:-:|---------:|---:|\n| `Japgolly` | ✅ | 199,745 | 1 |\n| `Jsii` | ✅ | 114,318 | (subtoken of `Jsii_commentary`) |\n| `Jsii_commentary` | — (3 toks) | — | 2 |\n| `changedFiles` | — (2 toks) | — | 8 |\n| `RTLU` | — (2 toks) | — | 3 |\n\n`Japgolly` is in the last 0.13% of the vocabulary — the same family of\nGitHub-corpus residue that produced `SolidGoldMagikarp` in the 2023\nGPT-2 vocabulary (Rumbelow & Watkins). `SolidGoldMagikarp` itself\ntokenizes to 5 tokens in `o200k_base` — that specific token was retired,\nbut the class wasn't.\n\nFor the multi-token entries, the corpus-level signature is the surface\nstring; the underlying glitch trigger is a sub-token (e.g. `Jsii` inside\n`Jsii_commentary`). The detector list (`G` signal) keys on the surface\nstrings.\n\nStable across unrelated sessions. Treated as a high-precision detector\nsignal.\n\n### 2.5 Channel-word leakage\n\n`analysis` (5), `assistant` (5), `commentary` (3), `user` (1) appear\ndirectly preceding `to=`. Always bare words; never `<|channel|>analysis`\nor any other bracketed form. Consistent with §1 — the brackets are\nmasked, the words are not.\n\n### 2.6 Non-Latin spam residue\n\n96 marker hits, by script: CJK 40, Cyrillic 12, Telugu/Kannada/Malayalam\n18, Thai 8, Georgian 7, Armenian 7, Arabic 1. Recurring fragments are\nChinese gambling SEO (`大发时时彩`, `天天中彩票`), Georgian/Abkhaz junk,\nand Thai casino spam — well-known low-quality crawl residue.\n\nThis is the same script distribution observed in the controlled\nreproduction (§7.3), independent of the prompt's natural language.\n\n### 2.7 Failure-mode breakdown for the `edit` tool\n\nThe `edit` tool exists in two variants in the corpus:\n\n| Variant | Calls | Recovery |\n|--------------------------|------:|----------|\n| Patch-DSL (`§PATH`/anchor/`«»≔` ops) | 27 | **Recoverable** by op-truncation (§3.3) |\n| JSON-schema (`{path,edits:[…]}`) | 11 | **Not recoverable** — contamination is escaped *inside* JSON strings, parser accepts it cleanly, content would be written verbatim into source files |\n\nFor Patch-DSL leaks specifically:\n\n- 20/27 cases: contamination on the last input line; nothing follows.\n- 7/27 cases: contamination mid-input; what follows is one of: a\n duplicate replay of an earlier file/anchor, intended content for a\n *different* tool call (the model started its next call inline), or\n pure hallucination. Post-contamination content is never trustworthy.\n\n### 2.8 Mechanism (confirmed)\n\n**Prior collapse from null-embedding glitch tokens, into a\ncontrol-token-masked basin whose mass redistributes onto the\nplain-text shadow of the Harmony protocol.**\n\nStep by step:\n\n1. The model is mid-`{ARGS}` of a Harmony tool call. The runtime applies\n a logit mask suppressing structural control tokens (`<|channel|>`,\n `<|message|>`, `<|call|>`, `<|start|>`, `<|end|>`) inside the args\n region. Without this mask, normal generation would constantly\n hallucinate envelope-closes; with it, those token IDs have logit\n `-∞` in args.\n2. A glitch token `g` is sampled. By construction `g` was in the BPE\n merge corpus but barely in LM/RL training, so its **input embedding\n `e_g` ≈ near-init noise of small norm**.\n3. At position t+1, the residual update `h_{t+1} ≈ LN(h_t + e_g + Attn +\n MLP)` is dominated by the prefix-derived terms; the just-emitted-token\n signal is effectively absent. Generation diversity normally comes\n from `e_x` steering the residual into different sub-regions —\n stripped here.\n4. The next-token distribution therefore collapses onto the **conditional\n prior over continuations of the prefix, with local conditioning\n removed**. In a tool-calling rollout context, that prior is sharply\n peaked on Harmony scaffolding (control tokens + routing tokens) —\n that's what RL trained.\n5. The mask zeros the control-token IDs. Mass redistributes onto the\n **next-best continuation**: the un-bracketed surface-form spelling of\n the same protocol (`analysis`, `commentary`, ` to=functions.X`,\n ` code `). This spelling is unmasked because those characters are\n ordinary tokens.\n6. Once a few tokens of plain-text scaffolding land in the residual\n stream, the prefix now resembles a fresh envelope start. The macro\n prior keeps voting for more scaffolding. Cascading (§2.3) follows.\n7. Multilingual spam after the marker is the same prior-collapse\n continuation, drawn from the training neighborhood of the glitch\n token (often ESL/auto-generated multilingual web junk — exactly the\n crawl residue in §2.6).\n\n**Two corollaries the corpus data demanded but only the experiment\nexplained:**\n\n- **The brackets never appear** (§1, §2.5). The mask is what makes the\n leak land in plain text instead of as a real envelope-close.\n- **Counterintuitive grammar dependency** (§7.4). The leak is *worse* in\n formats closest to OpenAI's training distribution. Off-distribution\n custom grammars dampen the macro-prior basin; the official\n `*** Begin Patch` format is the strongest collapse target.\n\nThe 2023 SolidGoldMagikarp paper documented mechanism (1)+(2)+(4). The\nnew piece is (5): when constrained decoding masks the natural collapse\ntarget, the mass laundered through the un-masked plain-text shadow\nbecomes a structurally-invisible exfiltration channel.",
@@ -22,6 +22,7 @@ export const EMBEDDED_DOCS: Readonly<Record<string, string>> = {
22
22
  "external-control-readiness.md": "# External control surface readiness\n\nThis document classifies every public GJC surface that an external controller, bot, editor, or harness can use to drive `gjc`. It is intentionally narrower than the generic bot guide: it states what is ready today, what is only editor/client-oriented, and what remains experimental.\n\n## Readiness matrix\n\n| Surface | Current readiness | Primary command | Use when | Do not use when | Provider-independent smoke path |\n| --- | --- | --- | --- | --- | --- |\n| Coordinator MCP | Preferred multi-session bot/control-plane surface. | `gjc mcp-serve coordinator` | A controller needs to start/register GJC sessions, send bounded turns, answer questions, read artifacts, and write durable status reports across one or more repo/worktree lanes. | The controller only needs one embedded subprocess and can own stdio directly. | `gjc mcp-serve coordinator --check --json`; `packages/coding-agent/test/coordinator-mcp.test.ts`; `packages/coding-agent/test/setup-cli.test.ts`. |\n| RPC stdio | Stable subprocess worker surface. | `gjc --mode rpc` | A host embeds one GJC worker process, sends JSONL commands over stdin, consumes stdout frames, and optionally uses `python/gjc-rpc`. | The host needs remote HTTPS, multi-session orchestration, or MCP tool discovery. | `packages/coding-agent/test/rpc-unattended-stdio.test.ts`; `packages/coding-agent/test/rpc-client.start.test.ts`; `packages/coding-agent/test/rpc-host-tools.test.ts`; `packages/coding-agent/test/rpc-host-uris.test.ts`. |\n| ACP mode | Editor/ACP client surface with tested protocol initialization, session lifecycle, client-owned MCP, file/terminal client bridges, permission routing, and stdout hygiene. | `gjc --mode acp` or `gjc acp` | An editor or ACP-compatible client wants to drive GJC through the Agent Client Protocol over stdio. | A bot needs a generic multi-session control plane; use Coordinator MCP instead. | `packages/coding-agent/test/acp-initialize-conformance.test.ts`; `packages/coding-agent/test/acp-stdout-hygiene.test.ts`; `packages/coding-agent/test/acp-lazy-startup.test.ts`; `packages/coding-agent/test/acp-mcp-isolation.test.ts`; `packages/coding-agent/test/read-acp-fs.test.ts`; `packages/coding-agent/test/write-acp-fs.test.ts`; `packages/coding-agent/test/bash-acp-terminal.test.ts`. |\n| Bridge HTTPS | Experimental, fail-closed remote session-control surface. | `gjc --mode bridge` | A future remote client needs HTTPS protocol scaffolding, authenticated health/help/handshake behavior, or SDK compatibility tests. | Production bot lifecycle, default external-controller integration, or claims that remote session events/commands are enabled by default. | `packages/coding-agent/test/bridge/bridge-auth.test.ts`; `packages/coding-agent/test/bridge/bridge-mode-handler.test.ts`; `packages/coding-agent/test/bridge/bridge-conformance.test.ts`; `packages/bridge-client/test/bridge-client.test.ts`. |\n\n## Surface details\n\n### Standalone TUI and MCP inheritance\n\nNormal standalone GJC (`gjc`, `gjc --tmux`, and print-mode prompts) does not inherit Claude Code, Codex, Cursor, Gemini, Windsurf, or other tools' MCP servers as a public startup contract. It also does not expose a supported standalone-TUI setting that automatically imports arbitrary MCP servers for the model. See [Standalone GJC MCP support](./standalone-mcp.md) for the user-facing boundary and workarounds.\n\n### Coordinator MCP\n\nCoordinator MCP is the default answer for external bot and orchestration integrations. It exposes a transport-level MCP tool contract for session discovery, managed session start, visible tmux registration, prompt delivery, bounded turn waiting, structured question answering, artifact reads, and explicit completion/failure/cancellation reports.\nIt also exposes high-level `gjc_delegate_plan` / `gjc_delegate_execute` / `gjc_delegate_team` tools so a host can delegate a whole GJC workflow (ralplan/ultragoal/team) in one call and consume the durable turn result. The canonical gajae-code plugin bundles under `plugins/` and `gjc setup claude|codex|hermes` package this surface with fail-closed defaults (workdir-scoped roots, mutations off until opt-in). Claude Code is installable through its generated local marketplace; Codex artifacts are preview-only until a versioned Codex local marketplace smoke proves install and runtime activation.\n\nReadiness claim:\n\n- Ready as the preferred generic external-controller control plane.\n- Provider-independent contract checks exist for server metadata, tool discovery, read-only defaults, mutation gates, setup rendering, and dry-run lifecycle behavior.\n- It is not a provider/model contract. Live model execution remains the operator's environment-specific smoke.\n\nPrimary references:\n\n- `docs/bot-integration.md`\n- `docs/hermes-mcp-bridge.md`\n- `packages/coding-agent/src/coordinator/contract.ts`\n- `packages/coding-agent/src/coordinator-mcp/server.ts`\n\n### RPC stdio\n\nRPC mode is the stable embedded-worker surface. It is newline-delimited JSON over stdio and emits a `{ \"type\": \"ready\" }` frame before accepting commands. Hosts can drive prompts, state queries, host tools, host URI schemes, workflow gates, extension UI responses, cancellation, and unattended negotiation through the RPC command catalog.\n\nReadiness claim:\n\n- Ready for single-process host integration and subprocess workers.\n- The public Python client in `python/gjc-rpc` is the recommended typed client for Python hosts.\n- Multi-session orchestration and MCP tool discovery are out of scope for RPC; use Coordinator MCP for those.\n\nPrimary references:\n\n- `docs/rpc.md`\n- `python/gjc-rpc/README.md`\n- `packages/coding-agent/src/modes/rpc/rpc-mode.ts`\n- `packages/coding-agent/src/modes/rpc/rpc-types.ts`\n\n### ACP mode\n\nACP mode runs GJC as an Agent Client Protocol server over stdio. It is useful for editor-style clients that own the ACP transport and want session creation, session load/fork/resume/close metadata, prompt handling, client-provided MCP servers, permission prompts, editor file reads/writes, terminal-backed bash, and elicitation support.\n\nReadiness claim:\n\n- ACP is implemented and covered for current editor/client contracts: initialize conformance, agent capability advertisement, lazy startup, stdout JSON-RPC hygiene, client-owned MCP isolation, event mapping, file bridge routing, terminal routing, and permission routing.\n- ACP is not the preferred bot control-plane surface. It is not positioned as a multi-session external bot coordinator, and it does not replace Coordinator MCP reports/artifacts/turn state.\n- A real prompt still depends on the selected provider/model credentials, so required PR smokes should stay on provider-independent initialize, lifecycle, bridge, and mapper tests.\n\nCurrent entrypoints:\n\n```sh\ngjc --mode acp\n# equivalent ACP subcommand for ACP clients that prefer command-style launch\ngjc acp\n```\nFor Zed custom-agent setup, add a custom `agent_servers` entry that launches the same stdio server explicitly:\n\n```json\n{\n \"agent_servers\": {\n \"gjc\": {\n \"type\": \"custom\",\n \"command\": \"gjc\",\n \"args\": [\"acp\"],\n \"env\": {}\n }\n }\n}\n```\n\nZed owns the ACP client connection and may forward editor-owned MCP servers over ACP; GJC keeps this isolated from standalone `.mcp.json` discovery and only starts ACP behavior through the explicit entrypoint.\n\nPrimary references:\n\n- `packages/coding-agent/src/commands/acp.ts`\n- `packages/coding-agent/src/modes/acp/acp-mode.ts`\n- `packages/coding-agent/src/modes/acp/acp-agent.ts`\n- `packages/coding-agent/src/modes/acp/acp-client-bridge.ts`\n- `packages/coding-agent/src/modes/acp/acp-event-mapper.ts`\n\n### Bridge HTTPS\n\nBridge mode is an experimental network protocol surface over HTTPS. Its current public posture is deliberately fail-closed: unauthenticated health/help are available, authenticated handshake is available, and default session-control endpoints advertise no accepted capabilities/scopes and reject with `endpoint_disabled`.\n\nReadiness claim:\n\n- Ready as experimental protocol scaffolding with fail-closed behavior and SDK/client conformance tests.\n- Not ready as the default external-bot product surface.\n- Do not document events, commands, controller ownership, UI responses, host tool results, or host URI results as enabled by default. Those names remain in the protocol catalog for internal compatibility and future re-enable work.\n\nPrimary references:\n\n- `docs/bridge.md`\n- `packages/coding-agent/src/modes/bridge/bridge-mode.ts`\n- `packages/coding-agent/src/modes/bridge/auth.ts`\n- `packages/bridge-client/src/index.ts`\n\n## PR smoke checklist\n\nFor external-control PRs, use this provider-independent checklist before any optional live provider smoke:\n\n1. **Docs-to-code alignment:** the readiness matrix still matches CLI mode parsing, MCP command registration, ACP command registration, bridge endpoint defaults, and RPC/ACP/Bridge tests.\n2. **Coordinator MCP:** `gjc mcp-serve coordinator --check --json` still reports the coordinator server and tool list, and focused MCP tests pass without provider credentials.\n3. **RPC stdio:** at least one stdio or client contract test proves JSONL startup/command routing without a real provider key.\n4. **ACP mode:** initialize/stdout or conformance tests prove the ACP JSON-RPC entrypoint and capability advertisement without a real provider key.\n5. **Bridge HTTPS:** bridge auth/handler tests prove TLS requirement, authenticated handshake, help/health behavior, and default `endpoint_disabled` session-control posture.\n6. **Local leak audit:** deliverable docs/tests must not contain private profile names, user-home paths, callback artifact paths, local proxy names, terminal app names, or private launch wrappers.\n\nOptional live smokes are useful diagnostics for one operator's model/profile/network setup, but they must not be required for PR readiness unless the PR explicitly changes live provider behavior.\n",
23
23
  "fs-scan-cache-architecture.md": "# Filesystem Scan Cache Architecture Contract\n\nThis document defines the current contract for the shared filesystem scan cache implemented in Rust (`crates/pi-natives/src/fs_cache.rs`) and consumed by native discovery/search APIs exposed to `packages/coding-agent`.\n\n## What this cache is\n\nThe cache stores full directory-scan entry lists (`GlobMatch[]`) keyed by scan scope and traversal policy, then lets higher-level operations (glob filtering, fuzzy scoring, grep file selection) run against those cached entries.\n\nPrimary goals:\n\n- avoid repeated filesystem walks for repeated discovery/search calls\n- keep consistency across `glob`, `fuzzyFind`, and `grep` when they share the same scan policy\n- allow explicit staleness recovery for empty results and explicit invalidation after file mutations\n\n## Ownership and public surface\n\n- Cache implementation and policy: `crates/pi-natives/src/fs_cache.rs`\n- Native consumers:\n - `crates/pi-natives/src/glob.rs`\n - `crates/pi-natives/src/fd.rs` (`fuzzyFind`)\n - `crates/pi-natives/src/grep.rs`\n- JS binding/export:\n - `packages/natives/src/glob/index.ts` (`invalidateFsScanCache`)\n - `packages/natives/src/glob/types.ts`\n - `packages/natives/src/grep/types.ts`\n- Coding-agent mutation invalidation helpers:\n - `packages/coding-agent/src/tools/fs-cache-invalidation.ts`\n\n## Cache key partitioning (hard contract)\n\nEach entry is keyed by:\n\n- canonicalized `root` directory path\n- `include_hidden` boolean\n- `use_gitignore` boolean\n- `skip_node_modules` boolean\n\nImplications:\n\n- Hidden and non-hidden scans do **not** share entries.\n- Gitignore-respecting and ignore-disabled scans do **not** share entries.\n- Scans that prune `node_modules` do **not** share entries with scans that include it.\n- Consumers must pass stable semantics for hidden/gitignore/node_modules behavior; changing any flag creates a different cache partition.\n\n## Scan collection behavior\n\nCache population uses a deterministic walker (`ignore::WalkBuilder`) configured by `include_hidden`, `use_gitignore`, and `skip_node_modules`:\n\n- `follow_links(false)`\n- sorted by file path\n- `.git` is always skipped\n- `node_modules` is pruned at traversal time when `skip_node_modules=true`\n- entry file type + `mtime` are captured via `symlink_metadata`\n\nSearch roots are resolved by `resolve_search_path`:\n\n- relative paths are resolved against current cwd\n- target must be an existing directory\n- root is canonicalized when possible\n\n## Freshness and eviction policy\n\nGlobal policy (environment-overridable):\n\n- `FS_SCAN_CACHE_TTL_MS` (default `1000`)\n- `FS_SCAN_EMPTY_RECHECK_MS` (default `200`)\n- `FS_SCAN_CACHE_MAX_ENTRIES` (default `16`)\n\nBehavior:\n\n- `get_or_scan(...)`\n - if TTL is `0`: bypass cache entirely, always fresh scan (`cache_age_ms = 0`)\n - on cache hit within TTL: return cached entries + non-zero `cache_age_ms`\n - on expired hit: evict key, rescan, store fresh entry\n- max entry enforcement is oldest-first eviction by `created_at`\n\n## Empty-result fast recheck (separate from normal hits)\n\nNormal cache hit:\n\n- a cache hit inside TTL returns cached entries and does nothing else.\n\nEmpty-result fast recheck:\n\n- this is a **caller-side** policy using `ScanResult.cache_age_ms`\n- if filtered/query result is empty and cached scan age is at least `empty_recheck_ms()`, caller performs one `force_rescan(...)` and retries\n- intended to reduce stale-negative results when files were recently added but cache is still within TTL\n\nCurrent consumers:\n\n- `glob`: rechecks when filtered matches are empty and scan age exceeds threshold\n- `fuzzyFind` (`fd.rs`): rechecks only when query is non-empty and scored matches are empty\n- `grep`: rechecks when selected candidate file list is empty\n\n## Consumer defaults and cache usage\n\nCache is opt-in on all exposed APIs (`cache?: boolean`, default `false`).\n\nCurrent defaults in native APIs:\n\n- `glob`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` included only when the pattern mentions `node_modules`\n- `fuzzyFind`: `hidden=false`, `gitignore=true`, `cache=false`, and `node_modules` is skipped\n- `grep`: `hidden=true`, `gitignore=true`, `cache=false`, and `node_modules` included only when the glob mentions `node_modules`\n\nCoding-agent callers today:\n\n- High-volume mention candidate discovery enables cache:\n - `packages/coding-agent/src/utils/file-mentions.ts`\n - profile: `hidden=true`, `gitignore=true`, `includeNodeModules=true`, `cache=true`\n- Tool-level `grep` integration currently disables scan cache (`cache: false`):\n - `packages/coding-agent/src/tools/grep.ts`\n\n## Invalidation contract\n\nNative invalidation entrypoint:\n\n- `invalidateFsScanCache(path?: string)`\n - with `path`: remove cache entries whose root is a prefix of target path\n - without path: clear all scan cache entries\n\nPath handling details:\n\n- relative invalidation paths are resolved against cwd\n- invalidation attempts canonicalization\n- if target does not exist (e.g., delete), fallback canonicalizes parent and reattaches filename when possible\n- this preserves invalidation behavior for create/delete/rename where one side may not exist\n\n## Coding-agent mutation flow responsibilities\n\nCoding-agent code must invalidate after successful filesystem mutations.\n\nCentral helpers:\n\n- `invalidateFsScanAfterWrite(path)`\n- `invalidateFsScanAfterDelete(path)`\n- `invalidateFsScanAfterRename(oldPath, newPath)` (invalidates both sides when paths differ)\n\nCurrent mutation tool callsites:\n\n- `packages/coding-agent/src/tools/write.ts`\n- `packages/coding-agent/src/patch/index.ts` (hashline/patch/replace flows)\n\nRule: if a flow mutates filesystem content or location and bypasses these helpers, cache staleness bugs are expected.\n\n## Adding a new cache consumer safely\n\nWhen introducing cache use in a new scanner/search path:\n\n1. **Use stable scan policy inputs**\n - decide hidden/gitignore/node_modules semantics first\n - pass them consistently to `get_or_scan`/`force_rescan` so cache partitions are intentional\n\n2. **Treat cache data as pre-filtered only by traversal policy**\n - apply tool-specific filtering (glob patterns, type filters, scoring) after retrieval\n - never assume cached entries already reflect your higher-level filters\n\n3. **Implement empty-result fast recheck only for stale-negative risk**\n - use `scan.cache_age_ms >= empty_recheck_ms()`\n - retry once with `force_rescan(..., store=true, ...)`\n - keep this path separate from normal cache-hit logic\n\n4. **Respect no-cache mode explicitly**\n - when caller disables cache, call `force_rescan(..., store=false, ...)`\n - do not populate shared cache in a no-cache request path\n\n5. **Wire mutation invalidation for any new write path**\n - after successful write/edit/delete/rename, call the coding-agent invalidation helper\n - for rename/move, invalidate both old and new paths\n\n6. **Do not add per-call TTL knobs**\n - current contract is global policy only (env-configured), no per-request TTL override\n\n## Known boundaries\n\n- Cache scope is process-local in-memory (`DashMap`), not persisted across process restarts.\n- Cache stores scan entries, not final tool results.\n- `glob`/`fuzzyFind`/`grep` share scan entries only when key dimensions (`root`, `hidden`, `gitignore`, `skip_node_modules`) match.\n- `.git` is always excluded at scan collection time regardless of caller options.\n",
24
24
  "geobench.md": "# GEO benchmark for Gajae-Code\n\nThis repository includes a [`geobench`](https://github.com/NomaDamas/geobench) product spec for measuring LLM answer visibility: hit rate, MRR, share of voice, citation rate/share, and confidence intervals.\n\n```bash\n/path/to/geobench/dist/geobench estimate --product geobench/gajae-code.yaml --providers openai --tier cheap\n/path/to/geobench/dist/geobench profile geobench/gajae-code.yaml\n/path/to/geobench/dist/geobench bench --product geobench/gajae-code.yaml --providers openai --tier cheap --mode benchmark\n```\n\nPublish aggregate metrics only; do not publish raw provider answers, secrets, or private run logs.\n",
25
+ "git-daemon.md": "# Git daemon\n\nThe git daemon is the autonomous per-repo service that watches a repository and resolves referenced work items by opening reviewed pull requests.\n",
25
26
  "gjc-dogfood-skill-template.md": "# GJC dogfood local skill template\n\nIssue #93 requested a gaebal-gajae/operator dogfood skill. The live issue has no comment approving a fifth bundled default workflow skill, so this stays a local template instead of changing the default workflow surface. Operators can copy it into a user or project override when they want GJC-first session guidance:\n\n```sh\nmkdir -p ~/.gjc/skills/gjc-dogfood\ncp docs/gjc-dogfood-skill-template.md ~/.gjc/skills/gjc-dogfood/SKILL.md\n```\n\nFor a single project, copy it to `<project>/.gjc/skills/gjc-dogfood/SKILL.md` instead. Do not commit that project `.gjc` copy unless the project explicitly wants a local override.\n\n---\nname: gjc-dogfood\ndescription: Use when running or reviewing work through GJC sessions, dogfooding Gajae-Code, or migrating an operator workflow from OMX to GJC.\n---\n\n# GJC Dogfood Operator Workflow\n\nUse GJC first for coding, review, planning, and follow-up sessions. Treat OMX as a fallback only when GJC is unavailable, broken, or missing a required capability.\n\n## Locate and launch GJC\n\n- Installed CLI: run `command -v gjc` and then launch with `gjc --tmux`.\n- Repository checkout: from the gajae-code repo, prefer `bun packages/coding-agent/src/cli.ts --tmux` when testing source changes before install.\n- Worktree isolation: for branch-specific work, either let GJC create a managed sibling worktree with `gjc --tmux --worktree <branch-like-name>` or `cd <existing-worktree-path>` and run `gjc --tmux` there. Do not pass filesystem paths to `--worktree`.\n- Name sessions explicitly with the project and issue, for example `gajae-code-93-dogfood-skill`, so tmux panes, logs, and exports remain traceable.\n\n## Start the session\n\n- Put git operations inside the GJC session: fetch, branch/worktree setup, focused commits, pushes, and PR creation should be visible in-session.\n- Submit the initial prompt with the issue URL, target branch, acceptance criteria, verification limits, and any existing plan/spec link.\n- Verify the prompt was accepted: the TUI should show the user prompt, an active assistant turn, or a tool/action request. If the session silently idles, resend once with a shorter prompt and capture the failure.\n- Verify working state before leaving the session unattended: confirm the target cwd/worktree, branch, and issue scope are visible in the transcript or command output.\n\n## During work\n\n- Keep session names and branch names issue-scoped.\n- Prefer GJC workflow skills only when they fit: `deep-interview` for unclear requirements, `ralplan` for planning, `ultragoal` for durable ledgers, and `team` for coordinated tmux execution.\n- Keep evidence in the session: issue reads, focused tests/checks, screenshots only when visual behavior matters, and PR URLs.\n- When GJC is weaker than OMX, finish the urgent work with the smallest safe fallback and file a gajae-code follow-up issue with the missing capability, exact command/session context, expected behavior, and evidence.\n\n## Fallback policy\n\nUse OMX or another operator path only when:\n\n- `gjc` cannot be located or launched after checking installed and repo-local commands;\n- authentication, model routing, tmux, or prompt submission is broken;\n- GJC lacks a required capability that OMX already has;\n- an urgent production/review deadline would be missed by debugging GJC first.\n\nRecord the fallback reason and create or link the gajae-code issue that would make GJC sufficient next time.\n\n## Evidence checklist\n\nReport:\n\n- project, issue, branch/worktree, and session name;\n- whether GJC was installed or repo-local;\n- prompt acceptance and working-state evidence;\n- git operations performed in-session;\n- focused verification commands and results;\n- PR/issue URLs;\n- follow-up gajae-code issues for any GJC gap or fallback.\n",
26
27
  "gjc-plugins.md": "# GJC Plugin Bundles\n\nGJC supports two distinct plugin families. Do not confuse them:\n\n1. **Legacy marketplace / npm plugins** (`packages/coding-agent/src/extensibility/plugins`) — installed through the existing `gjc plugin install <marketplace-ref|npm-spec>` marketplace/npm flows. Unchanged by this system.\n2. **GJC plugin bundles** — directories whose root contains a **`gajae-plugin.json`** manifest (`kind: \"gajae-code-plugin\"`). These *extend* existing GJC capabilities and are the subject of this document.\n\nA GJC plugin bundle may only **extend** existing skills/agents — it can never register a new top-level skill, slash-command, command, or agent. GJC exposes exactly four default workflow skills (`deep-interview`, `ralplan`, `team`, `ultragoal`) and four role agents (`executor`, `architect`, `planner`, `critic`); bundles add sub-skills/appendices/tools/hooks/MCPs to those existing parents only.\n\n## Manifest (`gajae-plugin.json`)\n\n```json\n{\n \"kind\": \"gajae-code-plugin\",\n \"name\": \"example-domain-bundle\",\n \"version\": \"1.0.0\",\n \"subskills\": [\"subskills/ralplan-design/SKILL.md\"],\n \"tools\": [\n { \"name\": \"domain_note\", \"path\": \"tools/domain-note.ts\", \"description\": \"...\" }\n ],\n \"hooks\": [\n { \"name\": \"audit-read\", \"event\": \"tool_call\", \"target\": \"read\", \"phase\": \"before\", \"path\": \"hooks/audit-read.ts\" }\n ],\n \"mcps\": [\n { \"name\": \"domain_docs\", \"transport\": \"stdio\", \"command\": \"bun\", \"args\": [\"mcp/domain-docs.ts\"], \"cwd\": \".\" }\n ],\n \"system_appendix\": [{ \"name\": \"domain-policy\", \"path\": \"prompts/system-appendix.md\" }],\n \"agent-appendix\": [{ \"agent\": \"executor\", \"name\": \"domain-executor\", \"path\": \"prompts/executor-appendix.md\" }]\n}\n```\n\n### Surfaces (the only allowed extension points)\n\n| Surface | Purpose | Additive rule |\n|---------|---------|---------------|\n| `subskills` | Inline sub-skills bound to an existing skill/agent (`binds_to`/`phase`/`activation_arg`) | Two-tier (see below) |\n| `tools` | Always-on custom tools (object entries) or legacy subskill-scoped string paths | Additive; manifest-declared name is authoritative, never overwrites an existing tool |\n| `hooks` | Constrained event hooks bound to a declared `event`/`target`/`phase` | Additive; run alongside built-ins, never replace |\n| `mcps` | MCP servers (`stdio`/`http`/`sse`) | Additive; server-name collisions are hard errors |\n| `system_appendix` | Lower-authority text appended to the default agent system prompt | Append-only, never overrides base |\n| `agent-appendix` | Lower-authority text appended to an existing role agent's prompt | Append-only per named agent |\n\n### Forbidden / unsupported keys\n\n- **Forbidden** (`forbidden_surface`): `skills`, `slash-commands`, `commands`, `agents` — bundles may not register new top-level definitions.\n- **Unsupported** (`unsupported_surface`): `mcp`, `mcpServers` (use the canonical `mcps`), and any unknown top-level key.\n\n## Installation\n\n```sh\ngjc plugin install <path|git-url|tarball> --user # install into the user root\ngjc plugin install <path|git-url|tarball> --project # install into the project root\n```\n\nExactly one of `--user` / `--project` is required for GJC plugin bundles (there is no default root). A source containing `gajae-plugin.json` is classified as a GJC bundle and routed to the bundle installer **before** the marketplace/npm path; non-bundle sources fall through to the legacy flow.\n\nInstall is **compile-validate-then-copy**:\n\n1. The bundle is compiled and validated **without importing any plugin code** (manifest, frontmatter, and declared files are read as bytes only).\n2. Collision and MCP security policy are enforced (the durable registry is the collision authority — never capability \"first-wins\").\n3. Only the validated, hashed files are copied into a temp sibling, then atomically renamed into place; the registry entry is written last under a per-scope lock. Nothing is mutated on failure.\n\nIdempotency: re-installing identical content is a no-op; different content requires `--force`.\n\n## Security model\n\n- **Install validation never executes plugin code.** Tool/hook names are manifest-declared; at runtime the loaded factory must return/register exactly the declared name/event or the surface is quarantined (`runtime_mismatch`).\n- **MCP policy** (install + runtime connect): HTTPS-only for `http`/`sse`; private/loopback/link-local/unique-local/multicast and the `169.254.169.254` metadata endpoint are denied across IPv4, IPv6, IPv4-mapped/compatible, zone-id and trailing-dot forms; URL credentials and CRLF headers are rejected; DNS is re-resolved before connect (rebinding defence). `stdio` servers are confined to the plugin root (allowed launchers `node`/`bun` or a root-confined executable; required bundled script argument; no eval/loader flags; no env expansion).\n- **Hooks** run through a *constrained* API: only a handler for the declared event may be registered. `registerCommand`, `sendMessage`, `appendEntry`, renderer registration, and shell `exec` are denied (`security_policy`). The broad first-party hook API is never exposed to bundle hooks.\n- **Appendices** render as lower-authority, delimited `<gjc-plugin-system-appendix>` / `<gjc-plugin-agent-appendix>` blocks appended after the base/project prompt; size-capped (8 KiB/appendix, 32 KiB total) fail-closed; content is escaped and control-char sanitized. They can never override base/developer instructions.\n- **Hash drift**: installed files are re-verified against the registry at session start; any drift quarantines the plugin (`runtime_mismatch`).\n\n## Sub-skills: Tier-1 vs Tier-2\n\n- **Tier-1 advertisement** (metadata-only): when a parent skill/agent prompt is built, installed sub-skills bound to it are advertised as a bounded list (`plugin` / `name` / `description` / `activation_arg` / `phase`; max 12 items, 200-char descriptions, 4 KiB block, with an overflow note). No body content; rendered only in the target parent prompt, never the global public-workflow surface.\n- **Tier-2 activation** (full body): on explicit activation (e.g. `deep-interview --autoresearch`) or an agent's contextual choice, the full sub-skill body is injected as a `<gjc-subskill>` block at the matching phase.\n\n## Registry, enablement, and quarantine\n\nEach scope keeps a durable `registry.json` recording per-plugin: name/version, source (`path`/`git`/`tarball` + ref/sha), manifest hash, copied files (relative path + sha256 — the uninstall ownership boundary), per-surface extension IDs, `enabled` flag, `disabledSurfaceIds`, and any `quarantine` entries.\n\nExtension IDs are stable: `tool:<name>`, `hook:<event>:<phase>:<target>:<name>`, `mcp:<name>`, `system-appendix:<plugin>:<name>`, `agent-appendix:<agent>:<plugin>:<name>`, `subskill:<parent>:<phase>:<activation_arg>`. Disabled is user-controlled (not an error); quarantine is fail-closed and visible.\n\n## Status / scope notes\n\n- Always-on **tools**, **system appendices**, **agent appendices**, and **Tier-1 advertisement** activate at session start (additive; no-op when no bundle is installed).\n- **MCP runtime connection** and the **live hook runner** integration are gated behind the same validated registry + policy; consult the ledger/run notes for their wiring status.\n- Full enable/disable/uninstall/upgrade UX is a planned follow-up; the registry already records everything required for it (per-surface IDs + copied-file ownership).\n",
27
28
  "gjc-session-clawhip-routing.md": "# Clawhip-routed GJC sessions\n\nThis guide documents the visible tmux session pattern used by operator bots such as Clawhip, Hermes, and OpenClaw when repository work must stay observable in a routed channel.\n\nUse this pattern when a human or chatops router needs to watch the session, receive stale-session alerts, and send follow-up prompts into the same visible GJC pane.\n\nFor pure machine control, prefer the Coordinator MCP tools in [`docs/hermes-mcp-bridge.md`](./hermes-mcp-bridge.md). For a single embedded worker process, prefer [`docs/rpc.md`](./rpc.md). This visible-session pattern is the operator-facing fallback/interop lane.\n\n## Contract\n\n1. Create or verify a dedicated git worktree for the issue or PR.\n2. Register a named tmux session with the host router before launching GJC.\n3. Start interactive `gjc` inside the worktree.\n4. Wait until the GJC TUI is ready.\n5. Inject the real task prompt separately.\n6. Verify acceptance from actual work evidence, not from a visible pasted prompt.\n\nDo not launch visible routed work in the canonical repo checkout. Use a worktree so branch changes, generated files, tests, and cleanup stay scoped to the task.\n\n## Session naming\n\nUse stable names that include the project and artifact id:\n\n```text\ngajae-code-issue-905-ctrl-shift-enter-newline\ngajae-code-pr-911-ctrl-shift-enter-review\nclawhip-issue-269-lightweight-zero-receipt\n```\n\nAvoid ambiguous names such as `fix-tui`, `review`, or `issue-905` when multiple repositories route into the same chat surface.\n\n## Portable script shape\n\nThe exact router command is host-owned. A Clawhip-style wrapper usually has three small scripts:\n\n```sh\n# create.sh\n# create/register a routed tmux session and start interactive gjc in the worktree\nscripts/gjc-session/create.sh <session-name> <worktree-path> [channel-id] [mention]\n\n# prompt.sh\n# inject the real task after the TUI is ready\nscripts/gjc-session/prompt.sh <session-name> @/path/to/task.md\n\n# tail.sh\n# inspect bounded pane output before/after prompt delivery\nscripts/gjc-session/tail.sh <session-name> [lines]\n```\n\nThis repository includes a portable implementation in `scripts/gjc-session/`. It keeps private routing values outside the script body: channel ids and mentions are runtime arguments, the router binary is optional, and credentials are never embedded. Host deployments can still override router behavior with environment variables instead of editing the scripts.\n\n\n## Included helper scripts\n\nThe `scripts/gjc-session/` directory contains the public version of the operator helpers:\n\n- `create.sh` validates a dedicated git worktree, starts interactive `gjc` in tmux, preserves the pane after exit, prints and writes the session-specific durable state path, writes `metadata.json`, mirrors pane output to `pane.log`, records lifecycle events in `events.log`, writes normal-exit `final.json`, and optionally registers a Clawhip-style `tmux watch`.\n- `prompt.sh` sends a text or `@file` prompt only after the pane looks like a ready GJC TUI; if the tmux session vanished, it refuses injection and prints the durable metadata/log/final/events recovery paths plus the last pane-log excerpt.\n- `tail.sh` captures bounded pane output for readiness and acceptance checks, with durable metadata, pane-log, event-log, and final-status fallback when tmux vanished.\n- `harness-tmux-owner-start.sh` starts the GJC harness control plane with the RuntimeOwner resident inside tmux for dogfood/debug cases that need visible owner liveness.\n\nConfiguration is runtime-only:\n\n```sh\nexport GJC_BIN=/path/to/gjc # optional; defaults to command -v gjc\nexport GJC_SESSION_FLAGS=\"--model provider/model\" # optional interactive gjc flags\nexport GJC_SESSION_ROUTER=clawhip # optional router binary\nexport GJC_SESSION_SKIP_ROUTER=1 # skip router registration\nexport GJC_SESSION_STATE_DIR=/tmp/gjc-session-state # optional durable metadata/log root\nexport GJC_SESSION_LOG_SEARCH_ROOT=$HOME/Workspace # optional tail/prompt fallback search root\nexport GJC_SESSION_STALE_MINUTES=60 # router stale window\nexport GJC_SESSION_KEYWORDS=\"/skill:ralplan,Question\"\n```\n\nNo token, channel id, mention, workspace root, or private host path is hard-coded. Pass channel/mention values at invocation time when your router needs them.\n\n## Example flow\n\n```sh\n# 1. Prepare a dedicated worktree.\ngit -C /repo/gajae-code fetch origin dev\ngit -C /repo/gajae-code worktree add \\\n /repo/worktrees/gajae-code-issue-905-ctrl-shift-enter-newline \\\n -b issue-905-ctrl-shift-enter-newline origin/dev\n\n# 2. Start the routed visible session.\n./scripts/gjc-session/create.sh \\\n gajae-code-issue-905-ctrl-shift-enter-newline \\\n /repo/worktrees/gajae-code-issue-905-ctrl-shift-enter-newline \\\n \"$CHANNEL_ID\" \\\n \"$MENTION\"\n\n# 3. Confirm TUI readiness.\n./scripts/gjc-session/tail.sh gajae-code-issue-905-ctrl-shift-enter-newline 80\n\n# 4. Inject the task prompt.\n./scripts/gjc-session/prompt.sh \\\n gajae-code-issue-905-ctrl-shift-enter-newline \\\n @/tmp/issue-905-task.md\n\n# 5. Confirm real work started.\n./scripts/gjc-session/tail.sh gajae-code-issue-905-ctrl-shift-enter-newline 160\n```\n\n## Prompt shape\n\nImplementation prompt:\n\n```text\n/skill:ralplan\n\ngjc ultragoal fix issue #905 missed Ctrl+Shift+Enter newline case.\n\nRepo: Yeachan-Heo/gajae-code\nWorktree: /repo/worktrees/gajae-code-issue-905-ctrl-shift-enter-newline\nBranch: issue-905-ctrl-shift-enter-newline\nBase: dev\n\nScope:\n- inspect parser/key matching and packages/tui/src/components/editor.ts\n- add explicit ctrl+shift+enter newline handling\n- add focused tests for the reported terminal sequences\n- run targeted verification\n- commit, push, and open a PR to dev\n\nNon-goals:\n- no unrelated tmux/session/process changes\n- no synchronous filesystem, process, tmux, network, or durable writes in keystroke paths\n```\n\nReview prompt:\n\n```text\n/skill:ralplan\n\nReview PR #911 as a red-team-only merge gate.\nInspect origin/dev...HEAD, changed files, CI, and contract risks.\nLook for blockers, regressions, test gaps, and hidden user-facing drift.\nPost MERGE_READY or REQUEST_CHANGES with evidence. Do not merge.\n```\n\n## Acceptance checks\n\nAfter prompt delivery, require one of these before reporting that the session is working:\n\n- a tool call or file read in the pane,\n- an explicit plan or todo update,\n- a diff or test command,\n- a GitHub comment/review/PR URL,\n- a terminal verdict such as `MERGE_READY` or `REQUEST_CHANGES`.\n\nA prompt being visible in tmux scrollback is not acceptance by itself. If tmux disappears before terminal verdict, inspect the state path printed by `create.sh`: `metadata.json` identifies the worktree/session, `pane.log` contains the mirrored transcript, `events.log` records launch/exit milestones, and `final.json` is present when `gjc` exited normally. Use `tail.sh <session-name> [lines]` to surface these artifacts without a live tmux server.\n\n## Anti-patterns\n\n- Starting `gjc -p` for long-running visible repo work.\n- Launching from the canonical repo checkout instead of a task worktree.\n- Running a long GJC/tmux session under a short shell timeout that can SIGKILL the owner process.\n- Treating tmux process existence as proof that the prompt was accepted.\n- Restarting a vanished session without first checking its durable metadata, pane log, event log, and final status.\n- Hard-coding private channel ids, bot mentions, or router tokens into public GJC docs.\n- Using this visible-session pattern when Coordinator MCP turn state is available and sufficient.\n",