Skip to content
Merged
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@
All notable changes to **basou** are recorded here. The project follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html) starting with v0.1.0.

## Unreleased

### Added

- `basou orient --refresh` imports every adapter first (writing provenance), then
renders a guaranteed-fresh position — a one-command way for a SessionStart hook
to close the freshness gate. Bare `basou orient` stays **read-only** (it never
imports; the dry-run probe only reports staleness), preserving the invariant
that a plain orient never mutates the store.

### Changed

- When `basou orient` has **confirmed** staleness (the probe counted uncaptured /
grown native sessions), the warning is now asserted, not hedged: 「古いです(未取り込み N 件)—
着手前に必ず `basou refresh`」instead of 「古いかもしれません」. basou knows there is
uncaptured work, so it states it plainly with an imperative remediation rather
than understating the urgency. The genuinely-uncertain cases keep their hedged
wording (an un-re-importable grown session: 「最新か確認できません」; an unrun probe:
「最新か確認するには…」).

## 0.22.0 — 2026-06-24

### Added
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/commands/orient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,23 @@ describe("basou orient", () => {
const out = captureStdout();
await doRunOrient({}, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("⚠️ 古いかもしれません。");
expect(stdout).toContain("⚠️ 古いです。");
expect(stdout).toContain("`basou refresh`");
});

it("--refresh imports first, so the same uncaptured session renders ✅ current (not stale)", async () => {
const repo = await setupInitedRepo();
// The exact stale scenario above, but --refresh pulls it in before rendering.
await writeCodexRolloutAt(repo, "codex-uncaptured");
const out = captureStdout();
await doRunOrient({ refresh: true }, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("✅ 取り込みは最新です。");
expect(stdout).not.toContain("⚠️ 古いです。");
// The session was actually imported (the arc/position reflects 1 session).
expect(stdout).toContain("codex");
});

it("--verbose appends raw freshness telemetry; the default view omits it", async () => {
const repo = await setupInitedRepo();
await placeSession(repo, { id: SES("S01"), source: "claude-code-import" });
Expand Down
26 changes: 20 additions & 6 deletions packages/cli/src/commands/orient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import {
renderCliError,
} from "../lib/error-render.js";
import { loadHostsConfig } from "../lib/hosts-config.js";
import { probeStaleness } from "../lib/provenance-actions.js";
import { probeStaleness, refreshAll } from "../lib/provenance-actions.js";
import { resolveBasouRootForCommand } from "../lib/repo-root.js";
import type { ImportContext } from "./import.js";

export type OrientOptions = { verbose?: boolean; quiet?: boolean };
export type OrientOptions = { verbose?: boolean; quiet?: boolean; refresh?: boolean };

export type OrientContext = ImportContext & {
/** Defaults to `() => new Date()`. Injectable for tests. */
Expand All @@ -31,16 +31,22 @@ export type OrientContext = ImportContext & {
/**
* Wire `basou orient` onto `program`. A read-first "where am I" command: it
* renders the current position, writes `.basou/orientation.md`, and prints the
* body to stdout by default. It writes NO provenance — a read-only dry-run probe
* checks for uncaptured native work so the "これは最新か" verdict is honest (use
* `basou refresh` to actually re-import). `--verbose` appends raw freshness
* telemetry under the verdict.
* body to stdout by default. By default it writes NO provenance — a read-only
* dry-run probe checks for uncaptured native work so the "これは最新か" verdict is
* honest (use `basou refresh` to actually re-import). `--refresh` opts into
* importing first (so a SessionStart hook can guarantee a fresh position in one
* command) while bare `orient` stays read-only. `--verbose` appends raw
* freshness telemetry under the verdict.
*/
export function registerOrientCommand(program: Command): void {
program
.command("orient")
.description("Show the workspace's current position (also writes .basou/orientation.md)")
.option("-q, --quiet", "Write the file without printing the body")
.option(
"--refresh",
"Import all adapters first (writes provenance), then show a guaranteed-fresh position; bare orient is read-only",
)
.option("-v, --verbose", "Show error causes")
.action(async (opts: OrientOptions) => {
await runOrient(opts);
Expand Down Expand Up @@ -77,6 +83,14 @@ export async function doRunOrient(options: OrientOptions, ctx: OrientContext): P
const probeCtx: ImportContext = { cwd: repositoryRoot };
if (ctx.claudeProjectsDir !== undefined) probeCtx.claudeProjectsDir = ctx.claudeProjectsDir;
if (ctx.codexSessionsDir !== undefined) probeCtx.codexSessionsDir = ctx.codexSessionsDir;

// `--refresh` (opt-in) imports every adapter first so the position is
// guaranteed current — the moat-safe way for a SessionStart hook to close the
// freshness gate without bare `orient` ever becoming a writer. The subsequent
// dry-run probe then reports fresh, so the verdict reads "current" honestly.
if (options.refresh === true) {
await refreshAll({ options: {}, ctx: probeCtx, paths, nowIso });
}
const staleness = await probeStaleness({ ctx: probeCtx, paths, nowIso });

// Federation (zero-network): merge other hosts' trails listed in
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/orientation/orientation-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,10 @@ describe("orientation-renderer", () => {
nowIso: FIXED_NOW_ISO,
staleness: { newSessions: 2, updatedSessions: 1 },
});
expect(result.body).toContain("⚠️ 古いかもしれません。");
expect(result.body).toContain("⚠️ 古いです。");
expect(result.body).toContain("新規 2 件");
expect(result.body).toContain("更新 1 件");
expect(result.body).toContain("`basou refresh`");
expect(result.body).toContain("着手前に必ず `basou refresh`");
});

it("これは最新か: an unrun probe (null) says it cannot confirm rather than claiming current", async () => {
Expand Down Expand Up @@ -445,7 +445,9 @@ describe("orientation-renderer", () => {
// The banner is uniquely identified by its trailing pointer to the verdict.
const bannerMarker = "(詳細は末尾「これは最新か」)";
expect(result.body).toContain(bannerMarker);
expect(result.body).toContain("> ⚠️ **古いかもしれません**");
// Confirmed stale: asserted, not hedged, with the uncaptured counts inline.
expect(result.body).toContain("> ⚠️ **古いです(未取り込み 新規 2 件・更新 1 件)**");
expect(result.body).toContain("着手前に必ず `basou refresh`");
// It must appear before the position section so a top-down reader meets it
// before the direction / next-step content.
expect(result.body.indexOf(bannerMarker)).toBeLessThan(result.body.indexOf("## 今どこにいる"));
Expand Down
13 changes: 10 additions & 3 deletions packages/core/src/orientation/orientation-renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1031,8 +1031,11 @@ function stalenessBanner(
const parts: string[] = [];
if (staleness.newSessions > 0) parts.push(`新規 ${staleness.newSessions} 件`);
if (staleness.updatedSessions > 0) parts.push(`更新 ${staleness.updatedSessions} 件`);
// Confirmed stale (the probe counted uncaptured work), so assert it rather
// than hedge with "かもしれません" — basou KNOWS there is work to pull in, and
// an imperative remediation is the high-salience signal the consumer needs.
return [
`> ⚠️ **古いかもしれません** — 未取り込みの作業があります(${parts.join("・")})。着手前に \`basou refresh\` で更新してください(詳細は末尾「これは最新か」)。`,
`> ⚠️ **古いです(未取り込み ${parts.join("・")})** — 着手前に必ず \`basou refresh\` を実行してください(詳細は末尾「これは最新か」)。`,
];
}
return [];
Expand Down Expand Up @@ -1069,9 +1072,13 @@ function freshnessVerdict(
const parts: string[] = [];
if (staleness.newSessions > 0) parts.push(`新規 ${staleness.newSessions} 件`);
if (staleness.updatedSessions > 0) parts.push(`更新 ${staleness.updatedSessions} 件`);
// Confirmed stale: the probe found uncaptured/grown native work, so state it
// plainly (not "かもしれません") with an imperative remediation. The hedged
// wording is reserved for the genuinely-uncertain branches (unverifiable, or
// an unrun probe) below/above.
return [
`⚠️ 古いかもしれません。最後の取り込み以降に未取り込みの作業があります(${parts.join("・")})。`,
"`basou refresh` で更新してください。",
`⚠️ 古いです。最後の取り込み以降に未取り込みの作業があります(${parts.join("・")})。`,
"着手前に必ず `basou refresh` を実行してください。",
];
}

Expand Down