Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 0 additions & 100 deletions context-save/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -947,106 +947,6 @@ Restore later with /context-restore.

---

<<<<<<< HEAD:checkpoint/SKILL.md.tmpl
## Resume flow

### Step 1: Find checkpoints

```bash
eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null)" && mkdir -p ~/.gstack/projects/$SLUG
CHECKPOINT_DIR="$HOME/.gstack/projects/$SLUG/checkpoints"
if [ -d "$CHECKPOINT_DIR" ]; then
find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | xargs ls -1t 2>/dev/null | head -20
else
echo "NO_CHECKPOINTS"
fi
```

List checkpoints from **all branches** (checkpoint files contain the branch name
in their frontmatter, so all files in the directory are candidates). This enables
Conductor workspace handoff — a checkpoint saved on one branch can be resumed from
another.

### Step 1.5: Check for WIP commit context (continuous checkpoint mode)

If `CHECKPOINT_MODE` was `"continuous"` during prior work, the branch may have
`WIP:` commits with structured `[gstack-context]` blocks in their bodies. These
are a second recovery trail alongside the markdown checkpoint files.

```bash
_BRANCH=$(git branch --show-current 2>/dev/null)
# Detect if this branch has any WIP commits against the nearest remote ancestor
_BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD origin/master 2>/dev/null)
if [ -n "$_BASE" ]; then
WIP_COMMITS=$(git log "$_BASE"..HEAD --grep="^WIP:" --format="%H" 2>/dev/null | head -20)
if [ -n "$WIP_COMMITS" ]; then
echo "WIP_COMMITS_FOUND"
# Extract [gstack-context] blocks from each WIP commit body
for SHA in $WIP_COMMITS; do
echo "--- commit $SHA ---"
git log -1 "$SHA" --format="%s%n%n%b" 2>/dev/null | \
awk '/\[gstack-context\]/,/\[\/gstack-context\]/ { print }'
done
else
echo "NO_WIP_COMMITS"
fi
fi
```

If `WIP_COMMITS_FOUND`: Read the extracted `[gstack-context]` blocks. Each block
represents a logical unit of prior work with Decisions/Remaining/Tried/Skill.
Merge these with the markdown checkpoint file to reconstruct session state. The
git history shows the chronological arc; the markdown checkpoint shows the
intentional save points. Both matter.

**Important:** Do NOT delete WIP commits during resume. They remain the recovery
trail until /ship squashes them into clean commits during PR creation.

### Step 2: Load checkpoint

If the user specified a checkpoint (by number, title fragment, or date), find the
matching file. Otherwise, load the **most recent** checkpoint.

Read the checkpoint file and present a summary:

```
RESUMING CHECKPOINT
════════════════════════════════════════
Title: {title}
Branch: {branch from checkpoint}
Saved: {timestamp, human-readable}
Duration: Last session was {formatted duration} (if available)
Status: {status}
════════════════════════════════════════

### Summary
{summary from checkpoint}

### Remaining Work
{remaining work items from checkpoint}

### Notes
{notes from checkpoint}
```

If the current branch differs from the checkpoint's branch, note this:
"This checkpoint was saved on branch `{branch}`. You are currently on
`{current branch}`. You may want to switch branches before continuing."

### Step 3: Offer next steps

After presenting the checkpoint, ask via AskUserQuestion:

- A) Continue working on the remaining items
- B) Show the full checkpoint file
- C) Just needed the context, thanks

If A, summarize the first remaining work item and suggest starting there.

---

=======
>>>>>>> origin/main:context-save/SKILL.md.tmpl
## List flow

### Step 1: Gather saved contexts
Expand Down
100 changes: 0 additions & 100 deletions context-save/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -198,106 +198,6 @@ Restore later with /context-restore.

---

<<<<<<< HEAD:checkpoint/SKILL.md.tmpl
## Resume flow

### Step 1: Find checkpoints

```bash
{{SLUG_SETUP}}
CHECKPOINT_DIR="$HOME/.gstack/projects/$SLUG/checkpoints"
if [ -d "$CHECKPOINT_DIR" ]; then
find "$CHECKPOINT_DIR" -maxdepth 1 -name "*.md" -type f 2>/dev/null | xargs ls -1t 2>/dev/null | head -20
else
echo "NO_CHECKPOINTS"
fi
```

List checkpoints from **all branches** (checkpoint files contain the branch name
in their frontmatter, so all files in the directory are candidates). This enables
Conductor workspace handoff — a checkpoint saved on one branch can be resumed from
another.

### Step 1.5: Check for WIP commit context (continuous checkpoint mode)

If `CHECKPOINT_MODE` was `"continuous"` during prior work, the branch may have
`WIP:` commits with structured `[gstack-context]` blocks in their bodies. These
are a second recovery trail alongside the markdown checkpoint files.

```bash
_BRANCH=$(git branch --show-current 2>/dev/null)
# Detect if this branch has any WIP commits against the nearest remote ancestor
_BASE=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD origin/master 2>/dev/null)
if [ -n "$_BASE" ]; then
WIP_COMMITS=$(git log "$_BASE"..HEAD --grep="^WIP:" --format="%H" 2>/dev/null | head -20)
if [ -n "$WIP_COMMITS" ]; then
echo "WIP_COMMITS_FOUND"
# Extract [gstack-context] blocks from each WIP commit body
for SHA in $WIP_COMMITS; do
echo "--- commit $SHA ---"
git log -1 "$SHA" --format="%s%n%n%b" 2>/dev/null | \
awk '/\[gstack-context\]/,/\[\/gstack-context\]/ { print }'
done
else
echo "NO_WIP_COMMITS"
fi
fi
```

If `WIP_COMMITS_FOUND`: Read the extracted `[gstack-context]` blocks. Each block
represents a logical unit of prior work with Decisions/Remaining/Tried/Skill.
Merge these with the markdown checkpoint file to reconstruct session state. The
git history shows the chronological arc; the markdown checkpoint shows the
intentional save points. Both matter.

**Important:** Do NOT delete WIP commits during resume. They remain the recovery
trail until /ship squashes them into clean commits during PR creation.

### Step 2: Load checkpoint

If the user specified a checkpoint (by number, title fragment, or date), find the
matching file. Otherwise, load the **most recent** checkpoint.

Read the checkpoint file and present a summary:

```
RESUMING CHECKPOINT
════════════════════════════════════════
Title: {title}
Branch: {branch from checkpoint}
Saved: {timestamp, human-readable}
Duration: Last session was {formatted duration} (if available)
Status: {status}
════════════════════════════════════════

### Summary
{summary from checkpoint}

### Remaining Work
{remaining work items from checkpoint}

### Notes
{notes from checkpoint}
```

If the current branch differs from the checkpoint's branch, note this:
"This checkpoint was saved on branch `{branch}`. You are currently on
`{current branch}`. You may want to switch branches before continuing."

### Step 3: Offer next steps

After presenting the checkpoint, ask via AskUserQuestion:

- A) Continue working on the remaining items
- B) Show the full checkpoint file
- C) Just needed the context, thanks

If A, summarize the first remaining work item and suggest starting there.

---

=======
>>>>>>> origin/main:context-save/SKILL.md.tmpl
## List flow

### Step 1: Gather saved contexts
Expand Down
22 changes: 15 additions & 7 deletions design/src/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ServeOptions {
type ServerState = "serving" | "regenerating" | "done";

export async function serve(options: ServeOptions): Promise<void> {
const { html, port = 0, hostname = '127.0.0.1', timeout = 600 } = options;
const { html, port = 0, hostname = "127.0.0.1", timeout = 600 } = options;

// Validate HTML file exists
if (!fs.existsSync(html)) {
Expand All @@ -70,11 +70,14 @@ export async function serve(options: ServeOptions): Promise<void> {
const url = new URL(req.url);

// Serve the comparison board HTML
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) {
if (
req.method === "GET" &&
(url.pathname === "/" || url.pathname === "/index.html")
) {
// Inject the server URL so the board can POST feedback
const injected = htmlContent.replace(
"</head>",
`<script>window.__GSTACK_SERVER_URL = '${url.origin}';</script>\n</head>`
`<script>window.__GSTACK_SERVER_URL = ${JSON.stringify(url.origin)};</script>\n</head>`,
);
return new Response(injected, {
headers: { "Content-Type": "text/html; charset=utf-8" },
Expand Down Expand Up @@ -130,7 +133,9 @@ export async function serve(options: ServeOptions): Promise<void> {

const isSubmit = body.regenerated === false;
const isRegenerate = body.regenerated === true;
const action = isSubmit ? "submitted" : (body.regenerateAction || "regenerate");
const action = isSubmit
? "submitted"
: body.regenerateAction || "regenerate";

console.error(`SERVE_FEEDBACK_RECEIVED: type=${action}`);

Expand Down Expand Up @@ -185,18 +190,21 @@ export async function serve(options: ServeOptions): Promise<void> {
if (!newHtmlPath || !fs.existsSync(newHtmlPath)) {
return Response.json(
{ error: `HTML file not found: ${newHtmlPath}` },
{ status: 400 }
{ status: 400 },
);
}

// Security: resolve symlinks and validate the reload path is within the
// allowed directory (anchored to the initial HTML file's parent).
// Prevents path traversal via /api/reload reading arbitrary files.
const resolvedReload = fs.realpathSync(path.resolve(newHtmlPath));
if (!resolvedReload.startsWith(allowedDir + path.sep) && resolvedReload !== allowedDir) {
if (
!resolvedReload.startsWith(allowedDir + path.sep) &&
resolvedReload !== allowedDir
) {
return Response.json(
{ error: `Path must be within: ${allowedDir}` },
{ status: 403 }
{ status: 403 },
);
}

Expand Down
72 changes: 40 additions & 32 deletions hosts/codex.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,73 @@
import type { HostConfig } from '../scripts/host-config';
import type { HostConfig } from "../scripts/host-config";

const codex: HostConfig = {
name: 'codex',
displayName: 'OpenAI Codex CLI',
cliCommand: 'codex',
cliAliases: ['agents'],
name: "codex",
displayName: "OpenAI Codex CLI",
cliCommand: "codex",
cliAliases: ["agents"],

globalRoot: '.codex/skills/gstack',
localSkillRoot: '.agents/skills/gstack',
hostSubdir: '.agents',
globalRoot: ".codex/skills/gstack",
localSkillRoot: ".agents/skills/gstack",
hostSubdir: ".agents",
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
mode: "allowlist",
keepFields: ["name", "description"],
descriptionLimit: 1024,
descriptionLimitBehavior: 'error',
descriptionLimitBehavior: "error",
},

generation: {
generateMetadata: true,
metadataFormat: 'openai.yaml',
skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec
metadataFormat: "openai.yaml",
skipSkills: ["codex"], // Codex skill is a Claude wrapper around codex exec
propagateSubdirs: ["references"],
},

pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' },
{ from: '.claude/skills/gstack', to: '.agents/skills/gstack' },
{ from: '.claude/skills/review', to: '.agents/skills/gstack/review' },
{ from: '.claude/skills', to: '.agents/skills' },
{ from: "~/.claude/skills/gstack", to: "$GSTACK_ROOT" },
{ from: ".claude/skills/gstack", to: ".agents/skills/gstack" },
{ from: ".claude/skills/review", to: ".agents/skills/gstack/review" },
{ from: ".claude/skills", to: ".agents/skills" },
],

suppressedResolvers: [
'DESIGN_OUTSIDE_VOICES', // design.ts:485 — Codex can't invoke itself
'ADVERSARIAL_STEP', // review.ts:408 — Codex can't invoke itself
'CODEX_SECOND_OPINION', // review.ts:257 — Codex can't invoke itself
'CODEX_PLAN_REVIEW', // review.ts:541 — Codex can't invoke itself
'REVIEW_ARMY', // review-army.ts:180 — Codex shouldn't orchestrate
'GBRAIN_CONTEXT_LOAD',
'GBRAIN_SAVE_RESULTS',
"DESIGN_OUTSIDE_VOICES", // design.ts:485 — Codex can't invoke itself
"ADVERSARIAL_STEP", // review.ts:408 — Codex can't invoke itself
"CODEX_SECOND_OPINION", // review.ts:257 — Codex can't invoke itself
"CODEX_PLAN_REVIEW", // review.ts:541 — Codex can't invoke itself
"REVIEW_ARMY", // review-army.ts:180 — Codex shouldn't orchestrate
"GBRAIN_CONTEXT_LOAD",
"GBRAIN_SAVE_RESULTS",
],

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'],
globalSymlinks: [
"bin",
"browse/dist",
"browse/bin",
"gstack-upgrade",
"ETHOS.md",
],
globalFiles: {
'review': ['checklist.md', 'TODOS-format.md'],
review: ["checklist.md", "TODOS-format.md"],
},
},
sidecar: {
path: '.agents/skills/gstack',
symlinks: ['bin', 'browse', 'review', 'qa', 'ETHOS.md'],
path: ".agents/skills/gstack",
symlinks: ["bin", "browse", "review", "qa", "ETHOS.md"],
},

install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
linkingStrategy: "symlink-generated",
},

coAuthorTrailer: 'Co-Authored-By: OpenAI Codex <noreply@openai.com>',
learningsMode: 'basic',
boundaryInstruction: 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.',
coAuthorTrailer: "Co-Authored-By: OpenAI Codex <noreply@openai.com>",
learningsMode: "basic",
boundaryInstruction:
"IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.",
};

export default codex;
Loading