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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@ All notable changes to **basou** are recorded here. The project follows

### Added

- The generated views — `handoff.md`, `orientation.md`, `decisions.md`, and
`basou report generate` — now localize their tool-generated chrome (section
headings, labels, freshness-verdict prose) per workspace: the view language
follows the manifest anchor repo's declared `language` ("the workspace speaks
the anchor's language"). `language: ja` on the anchor (the `repos[]` entry
whose `path` is `.`) renders the Japanese chrome exactly as before; `en`,
`en+ja`, an undeclared language, or no roster render English (the new
default). User data — decision titles, notes, labels, file paths — always
passes through verbatim; only the chrome is localized, and there is no
env-var / CLI-flag override (committed team artifacts speak the project's
declared language, not the caller's). All view strings now live in a single
`@basou/core` locale table (`viewStrings` / `resolveViewLanguage`, both
exported), which also shrinks the language-lint E-5 allowlist from four
renderers to that one module. The Codex context channel (`~/.codex/AGENTS.md`)
follows automatically since it carries the orientation body.

### Changed

- **Default view language is now English.** A workspace that never declared a
roster (or whose anchor has no `language`) previously rendered Japanese
headings; it now renders English. To keep the Japanese chrome, declare
`language: ja` on the anchor entry (`path: "."`) under `repos:` in
`.basou/manifest.yaml`. Rendered prose is presentation, not contract
(see `docs/spec/compatibility.md`).

- `basou view --check` now flags `redundant` portfolio entries — a registered
`~/.basou/portfolio.yaml` path that resolves to the same planning master as
another entry (its workspace view, or a member / source-root repo the master
Expand Down
90 changes: 60 additions & 30 deletions docs/spec/generated-markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,33 @@ reading provenance. They fall into two categories:
region to preserve) and it is also printed to stdout. See §10.7.

All four share the same surface convention: an English document title, a
`> Generated at <iso>` line, and Japanese `##` section headings with
mostly-English inline content.
`> Generated at <iso>` line, and localized `##` section headings (see the view
language rule below).

## §10.0 View language ("the workspace speaks the anchor's language")

The tool-generated chrome of all four views — section headings, labels, and
verdict prose — is localized per workspace, resolved from the manifest roster:

- The view language follows the **anchor repo** — the `repos[]` entry whose
`path` is `.` (the planning/trail home the views live in). This is a
deliberate, documented coupling between the anchor's declared audience and
the views' audience; other repos' languages do not participate.
- `language: ja` on the anchor renders Japanese chrome; `en` renders English.
- `en+ja` resolves to **English**: a generated view has exactly one chrome
language, and English is the shared floor of a bilingual audience.
- When no roster, no anchor entry, or no `language` is declared — or the
manifest is missing or unreadable — the fallback is **English**, silently:
rendering never fails or warns over language resolution.

Only the tool-generated strings are localized. User data — decision titles,
notes, session labels, file paths — always passes through verbatim, whatever
language it is written in. There is no environment-variable or CLI-flag
override: `handoff.md` / `decisions.md` are committed team artifacts, so their
language is a project declaration (the manifest), not a caller preference.

The rendered prose itself (headings, wording) is presentation, not contract —
see `compatibility.md`. The templates below show the English chrome.

## §10.1 Living-artifact policy (handoff.md, decisions.md)

Expand Down Expand Up @@ -56,31 +81,36 @@ text on the marker line is accepted to support legacy variants.

## Current state

- Latest session: ses_01HX...C (completed)
- Latest task: task_01HX_lp_form (in_progress)
- Last session: ses_01HX...C (completed)
- Last task: task_01HX_lp_form (in_progress)

## Recently changed files

- src/components/ContactForm.tsx
- src/styles/contact.css

## Recent decisions
## Latest decision

- Adopted zod for ContactForm validation (decision_01HX...)
- Adopted zod for ContactForm validation [decision_01HX...]

## Open questions
## Unresolved items

- Redirect destination after submission success
- 1 pending approvals

## Files to read next

- src/components/ContactForm.tsx
- .basou/decisions.md
- src/components/ContactForm.tsx

## Work to do next

- Finalize the post-submission UI flow (planned) [task_01HX...]

## Next steps
## Sessions

- Finalize the post-submission UI flow
- Add an E2E test
| short_id | status | started_at | label |
|---|---|---|---|
| 01HX...C | completed | 2026-05-04T12:00:00+09:00 | lp form |
<!-- BASOU:GENERATED:END -->

<!-- Below: human-authored notes -->
Expand All @@ -104,16 +134,16 @@ These fields originate from `decision_recorded` events.

## decision_01HX...: ContactForm validation

- **Date**: 2026-05-04
- **session**: ses_01HX...
- **Decision**: adopt zod
- **rationale**: integrates with the TypeScript type system; error messages
- date: 2026-05-04
- session: 01HX...
- decision: adopt zod
- rationale: integrates with the TypeScript type system; error messages
are easy to customize
- **alternatives**: yup, joi, hand-written validation
- **rejected_reason**: yup's TypeScript integration is weak; joi is
- alternatives: yup, joi, hand-written validation
- rejected_reason: yup's TypeScript integration is weak; joi is
overkill; hand-written is maintenance-heavy
- **linked_events**: evt_01HX..., evt_01HX...
- **linked_files**: src/components/ContactForm.tsx
- linked_events: evt_01HX..., evt_01HX...
- linked_files: src/components/ContactForm.tsx
<!-- BASOU:GENERATED:END -->

<!-- Below: human-authored notes -->
Expand Down Expand Up @@ -156,10 +186,9 @@ stdout (JSON-only, pipe-safe). `--title <text>` adds a subject line. A successfu
render always exits 0 — integrity verdicts inside the report are informational and
never fail the command (unlike `basou verify`).

**Sections** (in order): `## 概要` (summary), `## 作業量` (volume + time),
`## 判断` (decisions), `## 承認` (approvals), `## タスク` (tasks),
`## 変更ファイル` (changed files), `## セッション一覧` (sessions), `## 整合性`
(integrity). The markdown caps long lists with a `... +N more` line; the `--json`
**Sections** (in order): `## Summary`, `## Work volume` (volume + time),
`## Decisions`, `## Approvals`, `## Tasks`, `## Changed files`, `## Sessions`,
`## Integrity` (localized per §10.0). The markdown caps long lists with a `... +N more` line; the `--json`
shape always carries the full set. Changed files union only **non-import** sessions
(matching handoff), so cross-workspace round-trip imports do not dominate.

Expand All @@ -168,19 +197,19 @@ shape always carries the full set. Changed files union only **non-import** sessi

> Generated at 2026-05-09T03:00:00.000Z (2026-05-04..2026-05-08)

## 概要
## Summary

- Sessions: 12 (completed 9, failed 1, imported 2)
- Active time 6h 12m, 412,300 output tokens

## 作業量
## Work volume

- Output tokens: 412,300
- Actions: 84 commands, 37 files, 9 decisions
- Active time: 6h 12m (union; idle gaps > 5m excluded; tz UTC)
- Span: 31h 40m (total elapsed)

## 整合性
## Integrity

Provenance internally tamper-checked: 10 verified, 2 unchained, 0 empty, 0 incomplete, 0 in_progress, 0 tampered (of 12 sessions).

Expand Down Expand Up @@ -216,6 +245,7 @@ next intent only. It MUST NOT show per-agent scorecards, productivity
comparisons, or utilization — orientation is self-orientation about your own
product, not surveillance of the fleet.

**Sections** (in order): `## 今どこにいる` (where am I now), `## 何が動く`
(what is in flight — structured facts), `## どこへ向かう` (where am I heading),
`## これは最新か` (is this current — capture freshness / coverage).
**Sections** (in order): `## Where you are now`, `## Recent direction (last N
sessions)`, `## What is in flight` (structured facts), `## Where you are
heading`, `## Is this current` (capture freshness / coverage) — localized per
§10.0.
4 changes: 2 additions & 2 deletions packages/cli/src/commands/decisions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,8 @@ describe("basou decisions generate", () => {
await doRunDecisionsGenerate({}, { cwd: repo, nowProvider: () => FIXED_DATE });
const body = await readFile(basouPaths(repo).files.decisions, "utf8");
expect(body).toContain(`## ${did}: use zod`);
expect(body).toContain("- 決定日: 2026-05-08");
expect(body).toContain("- 判断: use zod");
expect(body).toContain("- date: 2026-05-08");
expect(body).toContain("- decision: use zod");
});

it("case 12: multiple decisions across sessions render chronologically", async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/handoff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe("basou handoff generate", () => {
expect(joinCalls(out)).toContain("sessions: 1");
});

it("case 3: 直近の変更ファイル shows only the most recent session's related_files", async () => {
it("case 3: Recently changed files shows only the most recent session's related_files", async () => {
const repo = await setupInitedRepo();
// Older session — its files must NOT appear once a newer session supersedes it.
await placeSession(repo, { id: SES("X02"), relatedFiles: ["src/a.ts"] });
Expand Down Expand Up @@ -356,7 +356,7 @@ describe("basou handoff generate", () => {
expect(joinCalls(err)).toContain("Markers mismatched in handoff.md");
});

it("case 11: pending approvals surface as a count in the 未決事項 section", async () => {
it("case 11: pending approvals surface as a count in the Unresolved items section", async () => {
const repo = await setupInitedRepo();
await placeSession(repo, { id: SES("X0A") });
await placePendingApproval(repo, APPR("A01"));
Expand Down
18 changes: 9 additions & 9 deletions packages/cli/src/commands/orient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ describe("basou orient", () => {
await doRunOrient({}, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("# Orientation");
expect(stdout).toContain("## 今どこにいる");
expect(stdout).toContain("## これは最新か");
expect(stdout).toContain("## Where you are now");
expect(stdout).toContain("## Is this current");

const body = await readFile(basouPaths(repo).files.orientation, "utf8");
expect(body).toContain("# Orientation");
Expand All @@ -207,7 +207,7 @@ describe("basou orient", () => {
await doRunOrient({ quiet: true }, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("Generated .basou/orientation.md");
expect(stdout).not.toContain("## 今どこにいる");
expect(stdout).not.toContain("## Where you are now");
const body = await readFile(basouPaths(repo).files.orientation, "utf8");
expect(body).toContain("# Orientation");
});
Expand Down Expand Up @@ -243,22 +243,22 @@ describe("basou orient", () => {
expect(joinCalls(err)).toContain("Workspace not initialized. Run 'basou init' first.");
});

it("これは最新か: a clean capture (no uncaptured native logs) prints the ✅ current verdict", async () => {
it("Is this current: a clean capture (no uncaptured native logs) prints the ✅ current verdict", async () => {
const repo = await setupInitedRepo();
await placeSession(repo, { id: SES("S01"), source: "claude-code-import" });
const out = captureStdout();
await doRunOrient({}, ctxFor(repo));
expect(joinCalls(out)).toContain("✅ 取り込みは最新です。");
expect(joinCalls(out)).toContain("✅ The capture is current.");
});

it("これは最新か: an uncaptured native session flips the verdict to ⚠️ stale → run refresh", async () => {
it("Is this current: an uncaptured native session flips the verdict to ⚠️ stale → run refresh", async () => {
const repo = await setupInitedRepo();
// A Codex rollout for this repo exists in the native logs but was never imported.
await writeCodexRolloutAt(repo, "codex-uncaptured");
const out = captureStdout();
await doRunOrient({}, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("⚠️ 古いです。");
expect(stdout).toContain("⚠️ Stale.");
expect(stdout).toContain("`basou refresh`");
});

Expand All @@ -269,8 +269,8 @@ describe("basou orient", () => {
const out = captureStdout();
await doRunOrient({ refresh: true }, ctxFor(repo));
const stdout = joinCalls(out);
expect(stdout).toContain("✅ 取り込みは最新です。");
expect(stdout).not.toContain("⚠️ 古いです。");
expect(stdout).toContain("✅ The capture is current.");
expect(stdout).not.toContain("⚠️ Stale.");
// The session was actually imported (the arc/position reflects 1 session).
expect(stdout).toContain("codex");
});
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe("basou report generate", () => {

const text = joinCalls(out);
expect(text).toContain("# Report");
expect(text).toContain("## 整合性");
expect(text).toContain("## Integrity");
expect(text).toContain("1 verified, 1 unchained");
expect(process.exitCode ?? 0).toBe(0);
});
Expand Down
40 changes: 30 additions & 10 deletions packages/core/src/decisions/decisions-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { stringify } from "yaml";
import { type BasouPaths, ensureBasouDirectory } from "../storage/basou-dir.js";
import { createManifest, writeManifest } from "../storage/manifest.js";
import { renderDecisions } from "./decisions-renderer.js";

const FIXED_WS_ID = "ws_01HXABCDEF1234567890ABCDEF" as const;
Expand Down Expand Up @@ -114,9 +115,9 @@ describe("decisions-renderer", () => {
const result = await renderDecisions({ paths, nowIso: FIXED_NOW_ISO });
expect(result.decisionCount).toBe(1);
expect(result.body).toContain(`## ${did}: use zod`);
expect(result.body).toContain("- 決定日: 2026-05-08");
expect(result.body).toContain("- date: 2026-05-08");
expect(result.body).toContain("- session:");
expect(result.body).toContain("- 判断: use zod");
expect(result.body).toContain("- decision: use zod");
});

it("case 3: aggregates decisions across multiple sessions in chronological order", async () => {
Expand Down Expand Up @@ -325,8 +326,8 @@ describe("decisions-renderer", () => {
);
const result = await renderDecisions({ paths, nowIso: FIXED_NOW_ISO });
expect(result.body).toContain(`## ${did}: legacy`);
expect(result.body).toContain("- 決定日: 2026-05-08");
expect(result.body).toContain("- 判断: legacy");
expect(result.body).toContain("- date: 2026-05-08");
expect(result.body).toContain("- decision: legacy");
expect(result.body).not.toContain("rationale:");
expect(result.body).not.toContain("alternatives:");
expect(result.body).not.toContain("linked_events:");
Expand Down Expand Up @@ -388,7 +389,7 @@ describe("decisions-renderer (voided decisions)", () => {
});

describe("decisions-renderer (track decisions)", () => {
it("marks an open track with [TRACK] and the 種別 line", async () => {
it("marks an open track with [TRACK] and the kind line", async () => {
const paths = await setupPaths();
const sid = SES("T01");
const did = DEC("T01");
Expand All @@ -403,11 +404,11 @@ describe("decisions-renderer (track decisions)", () => {
);
const result = await renderDecisions({ paths, nowIso: FIXED_NOW_ISO });
expect(result.body).toContain(`## ${did}: admin form coverage [TRACK]`);
expect(result.body).toContain("- 種別: track (close まで orient/handoff に継続表示)");
expect(result.body).toContain("- kind: track (stays in orient/handoff until closed)");
expect(result.body).toContain("- rationale: raw JSON is a stopgap");
});

it("a plain decision carries neither the [TRACK] marker nor the 種別 line", async () => {
it("a plain decision carries neither the [TRACK] marker nor the kind line", async () => {
const paths = await setupPaths();
const sid = SES("T02");
const did = DEC("T02");
Expand All @@ -420,10 +421,10 @@ describe("decisions-renderer (track decisions)", () => {
const result = await renderDecisions({ paths, nowIso: FIXED_NOW_ISO });
expect(result.body).toContain(`## ${did}: use zod`);
expect(result.body).not.toContain("[TRACK]");
expect(result.body).not.toContain("- 種別: track");
expect(result.body).not.toContain("- kind: track");
});

it("a voided track shows both [VOIDED] and [TRACK] and drops the 種別 (still-surfaced) line", async () => {
it("a voided track shows both [VOIDED] and [TRACK] and drops the kind (still-surfaced) line", async () => {
const paths = await setupPaths();
const sid = SES("T03");
const did = DEC("T03");
Expand All @@ -436,6 +437,25 @@ describe("decisions-renderer (track decisions)", () => {
expect(result.body).toContain(`## ~~${did}: closed track~~ [VOIDED] [TRACK]`);
expect(result.body).toContain("VOIDED: shipped");
// A closed track is no longer resurfaced, so it must not claim it is.
expect(result.body).not.toContain("close まで orient/handoff に継続表示");
expect(result.body).not.toContain("stays in orient/handoff until closed");
});
});

describe("renderDecisions (view language)", () => {
it("renders Japanese chrome when the manifest anchor declares language: ja", async () => {
const paths = await setupPaths();
const manifest = createManifest({ workspaceName: "fixture" });
manifest.repos = [{ path: ".", language: "ja" }];
await writeManifest(paths, manifest);
await placeSession(
paths,
SES("S01"),
"2026-05-08T11:00:00+09:00",
decisionLine(SES("S01"), "E01", DEC("D01"), "use zod", "2026-05-08T12:00:00+09:00"),
);
const result = await renderDecisions({ paths, nowIso: FIXED_NOW_ISO });
expect(result.body).toContain("- 決定日: 2026-05-08");
expect(result.body).toContain("- 判断: use zod");
expect(result.body).not.toContain("- decision:");
});
});
Loading