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
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ matching). Caller-unattested MCP keeps optional mutations unavailable. The CLI
(including `--confirm` without a TTY) return `needs_input` and never fabricate
an approval receipt.

YouTrack is an optional tracker adapter. Never hard-code greetings, `@mentions`,
people's names, organization hosts, issue ids, meeting wording or a default
timezone in core or tests; they come from `youtrack.json`, and comment wording
belongs in the editable `issue-update` template (a source scan in
`test/workit-core/youtrack-work-date.test.ts` guards hosts, issue-id literals
and hard-coded zones). Work-item dates are calendar days computed with
`Date.UTC` (process timezone unless `youtrack.json` sets `timezone`). The
effective zone is surfaced as `workTimezone` / `workDate` for agents, but never
inside the hashed approval descriptor, which must not vary by process timezone.

Read-only `workit_context` is available on OpenCode; Pi and the CLI use
`context.read` for the
enumerated git/PR/YouTrack/changelog/release/affected contexts without
Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,34 @@ or read separate VCS token files. YouTrack retains its permanent token. Existing
VCS token files and templates stay untouched. Project setup defaults to No:
press `n` to skip adding files when configuring from a parent folder containing
multiple repositories. Press `y` only to add hygiene files and gitignore entries
to the displayed directory. Locale and timezone keep their existing selections
until changed. `workit doctor` checks the configured installation.
to the displayed directory. Locale keeps its existing selection until changed.
`workit doctor` checks the configured installation.

YouTrack is optional and everything organization-specific comes from
`youtrack.json`; there are no built-in hosts, issues or wording:

- `baseUrl` is required. Without it the token-create link is unavailable and
the error names the config file.
- `meetingIssue` / `meetingIssues` choose the meeting issue(s); meetings mode
asks for one when none is configured. Meeting time uses each entry's
`workItemText`, else a global `meetingWorkItemText`, else `Meetings`.
- Work-item dates are a calendar day sent as that day's UTC midnight. "auto"
means today in the process timezone (honouring `TZ`); an IANA `timezone` in
`youtrack.json` overrides it. YouTrack context reports the effective zone as
`workTimezone: { timezone, source }` (`source` is `youtrack.json` or
`process`). Resolved `youtrack.update` / `youtrack.meeting` /
`youtrack.time` actions (and `workit action --preview`) report
`workDate: { localDate, timezone, timezoneSource }` beside the approval
descriptor, never inside it, so an approval matches in any process
timezone. An explicit epoch `dateMs` is labelled with its UTC day.
- Workit adds no greeting or `@mention` to comments. The text comes from the
editable `issue-update` template (`templates/issue-update.md` in the config
directory overrides the bundled neutral one); placeholders Workit does not
fill, such as a legacy `{{greetingSection}}`, render empty.

Older configs load unchanged: a `timezone` in the global `config.json`, and
`defaultMention`, `greetings` or `greetingCutoff` in `youtrack.json`, are
ignored.

`workit cutover` is for migrating legacy installations.

Expand Down
4 changes: 2 additions & 2 deletions docs/adaptive-workit/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ focused question only when one of those choices blocks a concrete slice.
### Completed: host recovery and reconciliation

- **Changes:** OpenCode v2.0.18's global config had one Workit registration,
`file:///home/cristhofer-pincetti/Documents/projects/personal/workflow-toolkit/packages/workit-opencode`.
`file://<checkout>/packages/workit-opencode`.
Removed only that entry with `opencode plugin remove`; backed up the original
to `/home/cristhofer-pincetti/.local/state/opencode/workit-recovery/opencode.json.pre-workit-disable.20260927T150744Z.bak`.
to `~/.local/state/opencode/workit-recovery/opencode.json.pre-workit-disable.<timestamp>.bak`.
Restarted the running service once so loaded hooks were dropped.
- **Checks:** the restarted service is healthy; `opencode plugin list` reports
"No plugins found"; `opencode debug config` shows only Command Code remains in
Expand Down
2 changes: 1 addition & 1 deletion packages/workit-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ workit uninstall # remove host registrations (keeps ~/
workit # help
```

`workit init` guides you through: detected host selection, basic global config (locale, timezone, branch policy), optional advanced commit policy, YouTrack, VCS, workspaces (scoped hosting/tracker/branch/commit rules, profiles and release tracks), and project hygiene files. The wizard is a TTY application — `workit init` requires an interactive terminal and prints guidance (exiting nonzero) when stdin is not a TTY.
`workit init` guides you through: detected host selection, basic global config (locale, branch policy), optional advanced commit policy, YouTrack, VCS, workspaces (scoped hosting/tracker/branch/commit rules, profiles and release tracks), and project hygiene files. The wizard is a TTY application — `workit init` requires an interactive terminal and prints guidance (exiting nonzero) when stdin is not a TTY.

Authenticate GitHub or GitLab with `gh auth login` or `glab auth login` before hosting actions; Workit does not need a second provider token file. `workit action` Git/hosting payloads accept `cwd` to target any checkout while task state stays in the session directory. Non-Git directories can host tasks for OS work; YouTrack keeps its own permanent token.

Expand Down
1 change: 0 additions & 1 deletion packages/workit-cli/assets/templates/greeting.md

This file was deleted.

3 changes: 1 addition & 2 deletions packages/workit-cli/assets/templates/issue-update.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Actualización
{{greetingSection}}
# Update
{{projectSection}}
{{userNotesSection}}
{{progressSection}}
Expand Down
39 changes: 2 additions & 37 deletions packages/workit-cli/src/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,6 @@ export function validateLocale(locale: string): string | null {
return null;
}

const KNOWN_TIMEZONES: string[] | null =
typeof Intl.supportedValuesOf === "function" ? Intl.supportedValuesOf("timeZone") : null;

export function validateTimezone(timezone: string): string | null {
const tz = timezone.trim();
if (!tz) return "timezone is required";
if (KNOWN_TIMEZONES && !KNOWN_TIMEZONES.includes(tz)) {
return `unknown timezone "${tz}" — check the IANA name (e.g. America/Santiago)`;
}
return null;
}

export function validateBaseUrl(url: string): string | null {
let parsed: URL;
try {
Expand All @@ -59,7 +47,6 @@ export function validateBaseUrl(url: string): string | null {

export type ConfigInput = {
locale?: string;
timezone?: string;
preset?: BranchPreset;
allowed?: string[];
protectedNames?: string[];
Expand Down Expand Up @@ -487,7 +474,7 @@ export function isSetupComplete(results: {
export function scaffoldYouTrack(
dir: string,
baseUrl: string,
opts: { locale?: string; timezone?: string } = {},
opts: { locale?: string } = {},
): YouTrackScaffold {
mkdirSync(dir, { recursive: true });
const youtrackJson = path.join(dir, "youtrack.json");
Expand Down Expand Up @@ -515,29 +502,7 @@ export function scaffoldYouTrack(
const config = {
baseUrl,
tokenFile: tokenPath,
timezone: opts.timezone ?? "America/Santiago",
locale: opts.locale ?? "es-CL",
defaultMention: "Alejandra.Flores",
greetings: { morning: "buenos días", afternoon: "buenas tardes" },
greetingCutoff: "12:00",
meetingIssue: "IRPT-12",
meetingIssues: {
general: {
issue: "IRPT-12",
label: "General meetings (Reuniones internas Team IRP)",
workItemText: "Reuniones",
},
web: {
issue: "NSXFT-21",
label: "Web meetings",
workItemText: "Reuniones web",
url: "https://enghouseamg.youtrack.cloud/projects/NSXFT/issues/NSXFT-21",
},
},
commentHeader: "# Actualización",
attachmentsHeaderImages: "## Adjunto capturas",
attachmentsHeaderFiles: "## Archivos adjuntos",
attachmentsHeaderMixed: "## Adjuntos",
locale: opts.locale ?? "en",
tokenDefaults: {
name: "workit",
description: "OpenCode workit — /wk-issue-update and /wk-meetings",
Expand Down
96 changes: 2 additions & 94 deletions packages/workit-cli/src/steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ export const BRANCH_PRESET_DESCRIPTIONS: Record<BranchPreset, string> = {
export const SCREEN_PLACEHOLDERS = {
youtrack: "e.g. https://example.youtrack.cloud",
localeOther: "e.g. en-US or es-CL",
timezoneOther: "e.g. America/Santiago",
branchAllowed: "e.g. feature/*, bugfix/*",
branchProtected: "e.g. main, develop",
workspaceName: "e.g. work",
Expand Down Expand Up @@ -225,56 +224,11 @@ const commitPresetOptions = [
{ label: "Auto-detect", value: "auto" },
];

// Timezone catalog: the runtime's full canonical IANA set when available,
// else a static fallback of common zones. Guard shape mirrors logic.ts
// KNOWN_TIMEZONES — validateTimezone enforces membership exactly when
// supportedValuesOf exists, so the picker then shows precisely that set;
// on the fallback path validation stays open and Other… covers the rest.
const TIMEZONE_FALLBACK = [
"UTC",
"America/New_York",
"America/Santiago",
"America/Bogota",
"America/Mexico_City",
"America/Sao_Paulo",
"America/Argentina/Buenos_Aires",
"Europe/London",
"Europe/Madrid",
"Europe/Berlin",
"Asia/Tokyo",
"Asia/Shanghai",
"Asia/Kolkata",
"Australia/Sydney",
];
const TIMEZONES: string[] =
typeof Intl.supportedValuesOf === "function"
? Intl.supportedValuesOf("timeZone")
: TIMEZONE_FALLBACK;
// Detected host zone seeds the picker preselection — no typing needed.
const DETECTED_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";

export function timezonePickerOptions(): { label: string; value: string }[] {
// Detected host zone heads the list so its preselection is visible in the
// first window without typing (the full IANA set alone would bury it).
return TIMEZONE_PICKER_OPTIONS;
}
// Built once at module load (TIMEZONES and DETECTED_TIMEZONE are already
// module-eval constants): rebuilding the IANA catalog per render made the
// screen's useMemo ineffective (Task 3 advisory).
const TIMEZONE_PICKER_OPTIONS: { label: string; value: string }[] = [
{ label: DETECTED_TIMEZONE, value: DETECTED_TIMEZONE },
...TIMEZONES.filter((timezone) => timezone !== DETECTED_TIMEZONE).map((timezone) => ({
label: timezone,
value: timezone,
})),
{ label: "Other…", value: "other" },
];
// Text screens cannot offer the 'b' back key (it is a printable character the
// TextInput consumes), so there Esc walks back to the parent select screen and
// cancel happens from select/confirm screens. Draft state survives either way.
const TEXT_SCREENS: ReadonlySet<WizardScreen> = new Set([
"localeOther",
"timezoneOther",
"branchAllowed",
"branchProtected",
"youtrack",
Expand All @@ -289,7 +243,7 @@ const TEXT_SCREENS: ReadonlySet<WizardScreen> = new Set([
// Screens whose SearchSelect owns printable input: a cold 'b' starts a search
// instead of navigating back; only a typed-then-cleared query hands 'b' back
// to the wizard's back-navigation.
const SEARCH_SCREENS: ReadonlySet<WizardScreen> = new Set(["locale", "timezone"]);
const SEARCH_SCREENS: ReadonlySet<WizardScreen> = new Set(["locale"]);

// Deterministic match-preview samples derived from the current project path:
// the project itself, its parent, and a synthetic child repo. Every accepted
Expand Down Expand Up @@ -922,7 +876,7 @@ export function Wizard({
else dispatch({ type: "cancel" });
} else if (input.toLowerCase() === "b" && !TEXT_SCREENS.has(draft.screen)) {
// While a search is live or being started on a SearchSelect screen
// (locale, timezone), 'b' belongs to the query; only a typed-then-cleared
// (locale), 'b' belongs to the query; only a typed-then-cleared
// search navigates back. Other screens keep plain 'b' back-navigation.
const search = searchRef.current;
const searchOwnsB = SEARCH_SCREENS.has(draft.screen) && !(search.typed && search.q === "");
Expand Down Expand Up @@ -1083,49 +1037,6 @@ function Screen({
<Text dimColor>Enter to continue · Esc Back</Text>
</Box>
);
case "timezone":
return (
<Box flexDirection="column" gap={1}>
<Text bold>Step 2 — Global config · Timezone</Text>
<Text dimColor>Timezone (IANA name):</Text>
<Text>
Current: <Text color="green">{draft.values.timezone}</Text>
</Text>
{/* Searchable timezone picker mirroring the locale screen: the
detected host zone is preselected, typing filters the IANA
catalog, Enter commits the highlighted row. Other… keeps the
existing validated custom-input flow (CA-04). */}
<SearchSelect
options={timezonePickerOptions()}
value={draft.values.timezone || DETECTED_TIMEZONE}
placeholder="Type to search timezones…"
onQueryChange={onSearchQueryChange}
onSelect={(value) => {
if (value === "other") dispatch({ type: "pickOther" });
else {
dispatch({ type: "set", field: "timezone", value });
dispatch({ type: "next" });
}
}}
/>
{draft.errors.timezone && <Text color="red">{draft.errors.timezone}</Text>}
<Text dimColor>Type to filter · Enter to continue · b Back · Esc Cancel</Text>
</Box>
);
case "timezoneOther":
return (
<Box flexDirection="column" gap={1}>
<Text bold>Step 2 — Global config · Timezone (custom)</Text>
<Text dimColor>Type an IANA timezone (e.g. America/Santiago):</Text>
<TextInput
placeholder={SCREEN_PLACEHOLDERS.timezoneOther}
onChange={(value) => dispatch({ type: "set", field: "timezone", value })}
onSubmit={() => dispatch({ type: "next" })}
/>
{draft.errors.timezone && <Text color="red">{draft.errors.timezone}</Text>}
<Text dimColor>Enter to continue · Esc Back</Text>
</Box>
);
case "branchPreset": {
const policy = effectivePolicy(draft.values);
return (
Expand Down Expand Up @@ -1556,9 +1467,6 @@ function Screen({
<Text>
Locale: <Text color="green">{draft.values.locale}</Text>
</Text>
<Text>
Timezone: <Text color="green">{draft.values.timezone}</Text>
</Text>
<Text>
Branch policy: <Text color="green">{policy.preset}</Text> — allowed:{" "}
{policy.allowed.join(", ")} · protected: {policy.protected.join(", ")}
Expand Down
1 change: 1 addition & 0 deletions packages/workit-cli/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ export async function runActionCommand(argv: string[], deps: TaskCliDeps = {}):
operation: normalized.operation,
descriptor,
payload: normalized.payload,
...(resolved.data.workDate ? { workDate: resolved.data.workDate } : {}),
});
if (json) jsonResult(outOf(deps), result);
else printHuman(result, deps);
Expand Down
Loading
Loading