fix(cron): gate --workdir flag on hasCronWorkdir capability

`HermesCapabilities.hasCronWorkdir` was added but never consumed: the
editor sheet always rendered the Workdir TextField and the view model
unconditionally appended `--workdir <path>` whenever the field was
non-empty. On a pre-v0.12 host argparse rejects the unknown flag and
the entire `cron create`/`cron edit` call fails.

Two-layer gate:
- CronJobEditor takes a `supportsWorkdir` flag and hides the field on
  pre-v0.12 hosts.
- CronView reads `\.hermesCapabilities` and forces the workdir argument
  to "" / nil when the capability is absent, so an editing-an-existing-
  job path that hydrates `form.workdir` from a pre-existing value can't
  smuggle the flag through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-05-01 13:21:35 +02:00
parent 11bb2bd0c3
commit 4a2ef74b74
+15 -5
View File
@@ -12,11 +12,16 @@ import ScarfDesign
struct CronView: View {
@State private var viewModel: CronViewModel
@State private var pendingDelete: HermesCronJob?
@Environment(\.hermesCapabilities) private var capabilitiesStore
init(context: ServerContext) {
_viewModel = State(initialValue: CronViewModel(context: context))
}
private var hasCronWorkdir: Bool {
capabilitiesStore?.capabilities.hasCronWorkdir ?? false
}
var body: some View {
VStack(spacing: 0) {
pageHeader
@@ -32,7 +37,7 @@ struct CronView: View {
.loadingOverlay(viewModel.isLoading, label: "Loading cron jobs…", isEmpty: viewModel.jobs.isEmpty)
.onAppear { viewModel.load() }
.sheet(isPresented: $viewModel.showCreateSheet) {
CronJobEditor(mode: .create, availableSkills: viewModel.availableSkills) { form in
CronJobEditor(mode: .create, availableSkills: viewModel.availableSkills, supportsWorkdir: hasCronWorkdir) { form in
viewModel.createJob(
schedule: form.schedule,
prompt: form.prompt,
@@ -41,7 +46,7 @@ struct CronView: View {
skills: form.skills,
script: form.script,
repeatCount: form.repeatCount,
workdir: form.workdir
workdir: hasCronWorkdir ? form.workdir : ""
)
viewModel.showCreateSheet = false
} onCancel: {
@@ -49,7 +54,7 @@ struct CronView: View {
}
}
.sheet(item: $viewModel.editingJob) { job in
CronJobEditor(mode: .edit(job), availableSkills: viewModel.availableSkills) { form in
CronJobEditor(mode: .edit(job), availableSkills: viewModel.availableSkills, supportsWorkdir: hasCronWorkdir) { form in
viewModel.updateJob(
id: job.id,
schedule: form.schedule,
@@ -60,7 +65,7 @@ struct CronView: View {
newSkills: form.skills,
clearSkills: form.clearSkills,
script: form.script,
workdir: form.workdir
workdir: hasCronWorkdir ? form.workdir : nil
)
viewModel.editingJob = nil
} onCancel: {
@@ -477,6 +482,9 @@ struct CronJobEditor: View {
let mode: Mode
let availableSkills: [String]
/// Pass `false` on pre-v0.12 hosts; the `--workdir` field is hidden and
/// the form's value is dropped when the parent calls `createJob`/`updateJob`.
let supportsWorkdir: Bool
let onSave: (FormState) -> Void
let onCancel: () -> Void
@@ -511,7 +519,9 @@ struct CronJobEditor: View {
formField("Deliver", text: $form.deliver, placeholder: "origin | local | discord:CHANNEL | telegram:CHAT", mono: true)
formField("Repeat", text: $form.repeatCount, placeholder: "Optional count")
formField("Script path", text: $form.script, placeholder: "Python script whose stdout is injected", mono: true)
formField("Workdir", text: $form.workdir, placeholder: "Absolute path; pulls AGENTS.md/CLAUDE.md context (v0.12+)", mono: true)
if supportsWorkdir {
formField("Workdir", text: $form.workdir, placeholder: "Absolute path; pulls AGENTS.md/CLAUDE.md context", mono: true)
}
if !availableSkills.isEmpty {
VStack(alignment: .leading, spacing: 4) {
Text("Skills")