Skip to content

Commit c150f6c

Browse files
committed
fix(plan): harden artifacts and build lifecycle
Clarify the plan skill with a complete artifact example and remove the obsolete todo dependencies field from the frontend contract. Add non-blocking Write diagnostics for malformed .plan.md artifacts so files remain saved while agents receive structured repair guidance. Improve plan builds by: - Binding active builds to their session and turn - Rejecting builds without valid todo IDs - Settling state when the owning turn terminates - Preventing builds with unsaved editor changes - Preserving the current agent and sending only the plan path
1 parent 15093b4 commit c150f6c

11 files changed

Lines changed: 635 additions & 94 deletions

File tree

src/crates/assembly/core/builtin_skills/plan/SKILL.md

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,37 @@ Produce a concise, evidence-backed implementation plan and leave project source
1010
## Workflow
1111

1212
1. Inspect the relevant code, configuration, documentation, and repository instructions using read-only tools. Resolve important behavior and ownership questions before drafting the plan.
13-
2. Ask the user only when missing information would materially change the approach. Present concrete options and put the recommended option first. Do not ask whether to proceed merely because research is complete.
13+
2. Ask the user when missing information would materially change the approach. Present concrete options and put the recommended option first. Do not ask whether to proceed merely because research is complete.
1414
3. Use `Task` only for read-only research that materially improves the plan. Keep delegated scopes bounded and synthesize the findings yourself.
15-
4. Write one plan artifact at `.bitfun/plans/<short-kebab-name>.plan.md`. Use a concise descriptive filename, and do not overwrite a different existing plan blindly.
16-
5. After a successful plan Write, stop tool use and link the plan file without repeating its contents.
15+
4. Use `Write` to create one plan artifact at `.bitfun/plans/<short-kebab-name>.plan.md`. Use a concise descriptive filename, and do not overwrite a different existing plan blindly.
16+
5. Once the plan artifact is created and meets the format and content requirements, stop tool use and link the file without repeating its contents.
1717

1818
## Plan Artifact
1919

20-
Use `Write` with a payload containing the path header followed by the complete file:
20+
Create the artifact using this complete file structure. The artifact consists of a YAML frontmatter and a non-empty Markdown body whose first line is a level-1 heading.
2121

22-
```text
23-
+++ .bitfun/plans/<short-kebab-name>.plan.md
24-
<complete plan file>
25-
```
26-
27-
The file must start with YAML frontmatter in this shape. Always include `todos`; use `todos: []` for a simple plan. Every todo starts as `pending`, has a stable kebab-case ID, and includes `dependencies`, using `[]` when it has none.
28-
29-
```yaml
22+
```markdown
3023
---
3124
name: Short Plan Name
3225
overview: One or two sentence overview
3326
todos:
3427
- id: stable-todo-id
3528
content: Specific actionable task
3629
status: pending
37-
dependencies: []
3830
---
31+
32+
# Short Plan Name
33+
34+
...
3935
```
4036

41-
Follow the frontmatter with a non-empty Markdown body whose first line is a level-1 heading. Keep the plan proportional to the request and cite specific workspace-relative files with Markdown links when useful.
37+
- Todos help break down complex plans into manageable, trackable tasks. Always include `todos`; use `todos: []` for a simple plan. Every todo starts as `pending` and has a stable kebab-case ID.
38+
- The plan should be concise and actionable. Focus on high-level meaningful decisions rather than low-level implementation details
39+
- Keep the plan proportional to the request and cite specific workspace-relative files with Markdown links when useful.
4240

4341
## Rules
4442

4543
- Do not edit project source, change configuration, run mutating commands, or otherwise implement the task while this planning workflow applies. The only permitted mutation is a `.bitfun/plans/*.plan.md` artifact.
46-
- If the plan Write fails or falls back under `.bitfun/tmp`, fix only the plan artifact write and retry; that fallback is not a completed plan.
47-
- For a requested revision, Read the existing plan first and then use Edit or Write only on that same `.bitfun/plans/*.plan.md` file. Do not create another plan card merely to revise it.
48-
- The successful plan Write is the final tool call for the planning turn.
49-
- Once the user explicitly approves the plan or asks to implement, leave this planning workflow and perform the requested work normally. This skill does not require an Agent or mode switch.
44+
- Before creating a new plan, inspect `.bitfun/plans` and choose an unused filename.
45+
- For a requested revision, Read the existing plan first and then use Edit or Write only on that same `.bitfun/plans/*.plan.md` file.
46+
- Once the user explicitly approves the plan or asks to implement, leave this planning workflow and perform the requested work normally.

src/crates/assembly/core/src/agentic/tools/implementations/file_write_tool.rs

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use super::plan_artifact_diagnostics::{
2+
diagnose_plan_artifact, is_plan_artifact_path, PlanArtifactIssue,
3+
};
14
use crate::agentic::tools::file_permissions::file_permission_intents_allowing_managed_plan_edits;
25
use crate::agentic::tools::file_read_state_runtime::{
36
assert_file_not_unexpectedly_modified, file_mutation_timestamp_ms, get_stored_file_read_state,
@@ -240,6 +243,7 @@ impl FileWriteTool {
240243
missing_path_fallback: bool,
241244
path_format_warning: Option<&str>,
242245
ignored_parameter_names: &[String],
246+
plan_artifact_issues: Option<&[PlanArtifactIssue]>,
243247
) -> ToolResult {
244248
let mut assistant_message = if missing_path_fallback {
245249
format!(
@@ -264,19 +268,42 @@ impl FileWriteTool {
264268
formatted_names
265269
));
266270
}
271+
if let Some(issues) = plan_artifact_issues.filter(|issues| !issues.is_empty()) {
272+
assistant_message.push_str("\nThe `.plan.md` artifact does not match the plan format:");
273+
for issue in issues {
274+
assistant_message.push_str("\n- ");
275+
assistant_message.push_str(&issue.message);
276+
}
277+
assistant_message.push_str(
278+
"\nUse Edit on the written file to fix these issues before finishing.",
279+
);
280+
}
281+
let mut data = json!({
282+
"file_path": logical_path,
283+
"bytes_written": outcome.bytes_written,
284+
"lines_written": outcome.lines_written,
285+
"success": true,
286+
"status": outcome.status.as_str(),
287+
"missing_path_fallback": missing_path_fallback,
288+
"rename_required": missing_path_fallback,
289+
"path_format_corrected": path_format_warning.is_some(),
290+
"path_format_warning": path_format_warning,
291+
"message": assistant_message,
292+
});
293+
if let Some(issues) = plan_artifact_issues {
294+
data["plan_format"] = json!({
295+
"valid": issues.is_empty(),
296+
"issues": issues
297+
.iter()
298+
.map(|issue| json!({
299+
"code": issue.code,
300+
"message": issue.message.as_str(),
301+
}))
302+
.collect::<Vec<_>>(),
303+
});
304+
}
267305
ToolResult::Result {
268-
data: json!({
269-
"file_path": logical_path,
270-
"bytes_written": outcome.bytes_written,
271-
"lines_written": outcome.lines_written,
272-
"success": true,
273-
"status": outcome.status.as_str(),
274-
"missing_path_fallback": missing_path_fallback,
275-
"rename_required": missing_path_fallback,
276-
"path_format_corrected": path_format_warning.is_some(),
277-
"path_format_warning": path_format_warning,
278-
"message": assistant_message,
279-
}),
306+
data,
280307
result_for_assistant: Some(assistant_message),
281308
image_attachments: None,
282309
}
@@ -543,6 +570,8 @@ impl Tool for FileWriteTool {
543570

544571
let resolved = context.resolve_tool_path(&file_path)?;
545572
let path_format_warning = Self::path_format_correction_warning(&resolved);
573+
let plan_artifact_issues =
574+
is_plan_artifact_path(&resolved.logical_path).then(|| diagnose_plan_artifact(&content));
546575
context.enforce_path_operation(ToolPathOperation::Write, &resolved)?;
547576
context
548577
.record_light_checkpoint(
@@ -562,6 +591,7 @@ impl Tool for FileWriteTool {
562591
missing_path_fallback,
563592
path_format_warning.as_deref(),
564593
&ignored_parameter_names,
594+
plan_artifact_issues.as_deref(),
565595
);
566596
return Ok(vec![result]);
567597
}
@@ -598,6 +628,7 @@ impl Tool for FileWriteTool {
598628
missing_path_fallback,
599629
path_format_warning.as_deref(),
600630
&ignored_parameter_names,
631+
plan_artifact_issues.as_deref(),
601632
);
602633
return Ok(vec![result]);
603634
}
@@ -634,6 +665,7 @@ impl Tool for FileWriteTool {
634665
missing_path_fallback,
635666
path_format_warning.as_deref(),
636667
&ignored_parameter_names,
668+
plan_artifact_issues.as_deref(),
637669
);
638670

639671
Ok(vec![result])
@@ -642,7 +674,7 @@ impl Tool for FileWriteTool {
642674

643675
#[cfg(test)]
644676
mod tests {
645-
use super::FileWriteTool;
677+
use super::{diagnose_plan_artifact, FileWriteTool};
646678
use crate::agentic::tools::file_tool_guidance::{
647679
file_tool_guidance_message, is_file_tool_guidance_message, FILE_TOOL_GUIDANCE_PREFIX,
648680
};
@@ -893,6 +925,39 @@ mod tests {
893925
assert!(validation.message.is_none());
894926
}
895927

928+
#[test]
929+
fn write_success_result_reports_non_blocking_plan_issues() {
930+
let logical_path = ".bitfun/plans/example.plan.md";
931+
let content = "---\nname: Example\noverview: Overview\ntodos: []\n---\n";
932+
let issues = diagnose_plan_artifact(content);
933+
934+
let result = FileWriteTool::write_success_result(
935+
logical_path,
936+
super::write_file_success_outcome(logical_path, false, content),
937+
false,
938+
None,
939+
&[],
940+
Some(&issues),
941+
);
942+
let ToolResult::Result {
943+
data,
944+
result_for_assistant,
945+
..
946+
} = result
947+
else {
948+
panic!("expected result");
949+
};
950+
951+
assert_eq!(data["success"], true);
952+
assert_eq!(data["plan_format"]["valid"], false);
953+
assert_eq!(
954+
data["plan_format"]["issues"][0]["code"],
955+
"missing_markdown_body"
956+
);
957+
let assistant_message = result_for_assistant.as_deref().unwrap_or_default();
958+
assert!(assistant_message.contains("does not match the plan format"));
959+
}
960+
896961
#[cfg(windows)]
897962
#[tokio::test]
898963
async fn write_result_reports_normalized_windows_drive_path() {
@@ -927,6 +992,7 @@ mod tests {
927992
false,
928993
Some(&warning),
929994
&[],
995+
None,
930996
);
931997
let ToolResult::Result {
932998
data,

src/crates/assembly/core/src/agentic/tools/implementations/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub mod miniapp_publish_tool;
5252
pub mod page_deploy_tool;
5353
#[cfg(feature = "tools-miniapp")]
5454
pub mod page_publish_tool;
55+
mod plan_artifact_diagnostics;
5556
#[cfg(feature = "tools-miniapp")]
5657
pub mod playbook_tool;
5758
#[cfg(feature = "tools-agent-control")]

0 commit comments

Comments
 (0)