vtcode-core 0.169.4

Core library for VT Code - a Rust-based terminal coding agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

const TASKS_DIR: &str = ".vtcode/tasks";
const CURRENT_TASK_FILE: &str = "current_task.md";
const CURRENT_SPEC_FILE: &str = "current_spec.md";
const CURRENT_CONTRACT_FILE: &str = "current_contract.md";
const CURRENT_EVALUATION_FILE: &str = "current_evaluation.md";
const CURRENT_SPRINT_CONTRACT_FILE: &str = "current_sprint_contract.md";
const CURRENT_OUTCOME_VERIFICATION_FILE: &str = "current_outcome_verification.md";
const CURRENT_FEATURE_LIST_FILE: &str = "current_feature_list.md";
const SUMMARY_PREVIEW_CHARS: usize = 280;

/// Return the path to the current task tracker file.
pub fn current_task_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_TASK_FILE)
}

/// Return the path to the current context reset manifest file.
pub fn current_context_reset_path(workspace_root: &Path) -> PathBuf {
    workspace_root
        .join(TASKS_DIR)
        .join(crate::core::agent::context_reset::CONTEXT_RESET_FILE)
}

/// Return the path to the current spec artifact file.
pub fn current_spec_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_SPEC_FILE)
}

/// Return the path to the current contract artifact file.
pub fn current_contract_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_CONTRACT_FILE)
}

/// Return the path to the current evaluation artifact file.
pub fn current_evaluation_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_EVALUATION_FILE)
}

/// Return the path to the current sprint contract artifact file.
///
/// The sprint contract is the pre-sprint negotiation artifact: the generator
/// and evaluator agree on scope, acceptance criteria, and out-of-scope items
/// before implementation begins. This follows the long-running harness pattern
/// where "vague user stories become testable contracts."
pub fn current_sprint_contract_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_SPRINT_CONTRACT_FILE)
}

/// Return the path to the current outcome verification artifact file.
///
/// The outcome verification records what commands were run to verify, what the
/// actual output was, and whether tests/build passed. This enforces "evaluate
/// outcomes, not claims" -- the agent cannot declare success without showing
/// actual verification output.
pub fn current_outcome_verification_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_OUTCOME_VERIFICATION_FILE)
}

/// Return the paths of all harness artifacts that currently exist on disk.
pub fn existing_harness_artifact_paths(workspace_root: &Path) -> Vec<PathBuf> {
    [
        current_spec_path(workspace_root),
        current_contract_path(workspace_root),
        current_evaluation_path(workspace_root),
        current_sprint_contract_path(workspace_root),
        current_outcome_verification_path(workspace_root),
        current_feature_list_path(workspace_root),
    ]
    .into_iter()
    .filter(|path| path.exists())
    .collect()
}

/// Read a short summary of the current spec artifact, or `None` if unavailable.
pub fn read_spec_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_spec_path(workspace_root), "Spec")
}

/// Like [`read_spec_summary`], but drop the file when it predates `not_before`.
pub fn read_spec_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
    read_markdown_summary_fresh(&current_spec_path(workspace_root), "Spec", not_before)
}

/// Read a short summary of the current contract artifact, or `None` if unavailable.
pub fn read_contract_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_contract_path(workspace_root), "Contract")
}

/// Read a short summary of the current evaluation artifact, or `None` if unavailable.
pub fn read_evaluation_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_evaluation_path(workspace_root), "Evaluation")
}

/// Like [`read_evaluation_summary`], but drop the file when it predates `not_before`.
pub fn read_evaluation_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
    read_markdown_summary_fresh(&current_evaluation_path(workspace_root), "Evaluation", not_before)
}

/// Like [`read_contract_summary`], but drop the file when it predates `not_before`.
pub fn read_contract_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
    read_markdown_summary_fresh(&current_contract_path(workspace_root), "Contract", not_before)
}

/// Like [`read_feature_list_summary`], but drop the file when it predates `not_before`.
pub fn read_feature_list_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
    read_markdown_summary_fresh(&current_feature_list_path(workspace_root), "FeatureList", not_before)
}

/// Like [`read_sprint_contract_summary`], but drop the file when it predates `not_before`.
pub fn read_sprint_contract_summary_fresh(workspace_root: &Path, not_before: Option<SystemTime>) -> Option<String> {
    read_markdown_summary_fresh(&current_sprint_contract_path(workspace_root), "SprintContract", not_before)
}

/// Like [`read_outcome_verification_summary`], but drop the file when it predates `not_before`.
pub fn read_outcome_verification_summary_fresh(
    workspace_root: &Path,
    not_before: Option<SystemTime>,
) -> Option<String> {
    read_markdown_summary_fresh(&current_outcome_verification_path(workspace_root), "OutcomeVerification", not_before)
}

/// Best-effort start time of `session_id`, used to reject leftover workspace
/// task artifacts that predate the session. Returns `None` when the session
/// directory is missing so callers can fall back to unfiltered reads.
pub fn session_artifact_cutoff(workspace_root: &Path, session_id: &str) -> Option<SystemTime> {
    // Canonical store path (full-length sanitize_id), plus the raw id as a
    // fallback for callers that never opened the store.
    let candidates = [
        vtcode_memory::session_directory(workspace_root, session_id),
        workspace_root.join(".vtcode").join("sessions").join(session_id),
    ];
    for dir in candidates {
        let Ok(metadata) = fs::metadata(&dir) else {
            continue;
        };
        // Prefer true creation time. Falling back to `modified()` would make
        // the cutoff "now" for a freshly touched dir and over-filter artifacts
        // legitimately written at session start.
        if let Ok(created) = metadata.created() {
            return Some(created);
        }
    }
    None
}

/// Archive a fully-checked `current_task.md` so a finished checklist cannot
/// describe the next session. Incomplete checklists stay in place.
///
/// Returns the archive path when a move happened.
pub fn archive_completed_current_task(workspace_root: &Path, session_id: &str) -> Result<Option<PathBuf>> {
    let task_path = current_task_path(workspace_root);
    let Ok(content) = fs::read_to_string(&task_path) else {
        return Ok(None);
    };
    let checklist: Vec<&str> = content
        .lines()
        .map(str::trim_start)
        .filter(|line| line.starts_with("- ["))
        .collect();
    let is_checked = |line: &str| line.starts_with("- [x]") || line.starts_with("- [X]");
    if checklist.is_empty() || !checklist.iter().all(|line| is_checked(line)) {
        return Ok(None);
    }
    let archive_dir = workspace_root.join(TASKS_DIR).join("archive");
    fs::create_dir_all(&archive_dir).with_context(|| format!("create task archive dir {}", archive_dir.display()))?;
    let archive_path = archive_dir.join(format!("current_task-{}.md", filename_safe_id(session_id, 64)));
    fs::rename(&task_path, &archive_path)
        .with_context(|| format!("archive completed task tracker to {}", archive_path.display()))?;
    Ok(Some(archive_path))
}

/// Filename-safe session id prefix for archive side-cars (not envelope names —
/// those use `sanitize_session_id`'s fixed 32-char contract).
fn filename_safe_id(id: &str, max_chars: usize) -> String {
    id.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '_' || c == '-' {
                c
            } else {
                '_'
            }
        })
        .take(max_chars)
        .collect()
}

/// Write the spec artifact content to disk and return the path.
pub async fn write_spec(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_spec_path(workspace_root);
    write_artifact(path.as_path(), content, "current spec").await?;
    Ok(path)
}

/// Write the evaluation artifact content to disk and return the path.
pub async fn write_evaluation(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_evaluation_path(workspace_root);
    write_artifact(path.as_path(), content, "current evaluation").await?;
    Ok(path)
}

/// Write the contract artifact content to disk and return the path.
pub async fn write_contract(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_contract_path(workspace_root);
    write_artifact(path.as_path(), content, "current contract").await?;
    Ok(path)
}

/// Read a short summary of the sprint contract artifact, or `None` if unavailable.
pub fn read_sprint_contract_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_sprint_contract_path(workspace_root), "SprintContract")
}

/// Write the sprint contract artifact content to disk and return the path.
///
/// The sprint contract is the pre-sprint negotiation artifact where generator
/// and evaluator agree on scope and acceptance criteria before code is written.
pub async fn write_sprint_contract(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_sprint_contract_path(workspace_root);
    write_artifact(path.as_path(), content, "sprint contract").await?;
    Ok(path)
}

/// Read a short summary of the outcome verification artifact, or `None` if unavailable.
pub fn read_outcome_verification_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_outcome_verification_path(workspace_root), "OutcomeVerification")
}

/// Return the path to the current feature list artifact file.
///
/// The feature list is a persistent artifact the planner creates and the
/// evaluator modifies during feedback-driven replanning. It lists the
/// project's features with their acceptance criteria, so each agent session
/// can pick up an incremental unit of work. Following the long-running
/// harness pattern: "the planner can achieve replanning by modifying external
/// files: feature_list, sprint_contract, known_issues, next_actions."
pub fn current_feature_list_path(workspace_root: &Path) -> PathBuf {
    workspace_root.join(TASKS_DIR).join(CURRENT_FEATURE_LIST_FILE)
}

/// Read a short summary of the feature list artifact, or `None` if unavailable.
pub fn read_feature_list_summary(workspace_root: &Path) -> Option<String> {
    read_markdown_summary(&current_feature_list_path(workspace_root), "FeatureList")
}

/// Write the feature list artifact content to disk and return the path.
pub async fn write_feature_list(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_feature_list_path(workspace_root);
    write_artifact(path.as_path(), content, "feature list").await?;
    Ok(path)
}

/// Write the outcome verification artifact content to disk and return the path.
///
/// This records actual verification commands and their output, enforcing
/// "evaluate outcomes, not claims" -- the agent must show proof of verification.
pub async fn write_outcome_verification(workspace_root: &Path, content: &str) -> Result<PathBuf> {
    let path = current_outcome_verification_path(workspace_root);
    write_artifact(path.as_path(), content, "outcome verification").await?;
    Ok(path)
}

async fn write_artifact(path: &Path, content: &str, label: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        tokio::fs::create_dir_all(parent)
            .await
            .with_context(|| format!("create {} directory {}", label, parent.display()))?;
    }

    tokio::fs::write(path, content)
        .await
        .with_context(|| format!("write {} {}", label, path.display()))?;
    Ok(())
}

fn read_markdown_summary(path: &Path, label: &str) -> Option<String> {
    let content = fs::read_to_string(path).ok()?;
    let lines = content
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .filter(|line| !line.starts_with('#'))
        .take(4)
        .collect::<Vec<_>>();
    if lines.is_empty() {
        return None;
    }

    let joined = lines.join(" | ");
    Some(format!("{label}: {}", truncate_summary(&joined)))
}

/// Grace applied when comparing artifact mtime to session start.
///
/// A spec written as a handoff before `vtcode` starts is still live for this
/// session; only leftovers from *earlier* sessions (days/weeks old) must be
/// dropped. 24h covers normal handoff workflows; the Jul-24 fixture class is
/// far outside it.
const ARTIFACT_FRESHNESS_GRACE: std::time::Duration = std::time::Duration::from_secs(24 * 60 * 60);

/// Read a markdown summary only when the file is at least as new as `not_before`.
///
/// Workspace-global task artifacts outlive their session. A leftover fixture
/// must not describe a later session's memory envelope or orient snapshot.
/// Files within [`ARTIFACT_FRESHNESS_GRACE`] of `not_before` still count as
/// live so a just-written handoff artifact is kept.
fn read_markdown_summary_fresh(path: &Path, label: &str, not_before: Option<SystemTime>) -> Option<String> {
    if let Some(not_before) = not_before {
        let modified = fs::metadata(path).ok()?.modified().ok()?;
        let stale_before = not_before
            .checked_sub(ARTIFACT_FRESHNESS_GRACE)
            .unwrap_or(SystemTime::UNIX_EPOCH);
        if modified < stale_before {
            return None;
        }
    }
    read_markdown_summary(path, label)
}

fn truncate_summary(text: &str) -> String {
    vtcode_commons::formatting::truncate_within(text, SUMMARY_PREVIEW_CHARS, "...")
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[tokio::test]
    async fn writes_and_summarizes_spec_and_evaluation_artifacts() {
        let temp = tempdir().expect("tempdir");

        write_spec(temp.path(), "# Spec\n\nBuild a stronger exec harness.\n\nKeep it resumable.\n")
            .await
            .expect("write spec");
        write_contract(temp.path(), "# Contract\n\n- Deliver the requested change.\n- Verify with cargo check.\n")
            .await
            .expect("write contract");
        write_evaluation(temp.path(), "# Evaluation\n\nVerdict: fail\n\nNeed another revision round.\n")
            .await
            .expect("write evaluation");

        let paths = existing_harness_artifact_paths(temp.path());
        assert_eq!(paths.len(), 3);
        assert_eq!(
            read_spec_summary(temp.path()),
            Some("Spec: Build a stronger exec harness. | Keep it resumable.".to_string())
        );
        assert_eq!(
            read_contract_summary(temp.path()),
            Some("Contract: - Deliver the requested change. | - Verify with cargo check.".to_string())
        );
        assert_eq!(
            read_evaluation_summary(temp.path()),
            Some("Evaluation: Verdict: fail | Need another revision round.".to_string())
        );
    }

    #[tokio::test]
    async fn writes_and_summarizes_sprint_contract() {
        let temp = tempdir().expect("tempdir");

        write_sprint_contract(
            temp.path(),
            "# Sprint Contract\n\nScope: implement login endpoint.\nAcceptance: POST /login returns JWT.\n",
        )
        .await
        .expect("write sprint contract");

        let paths = existing_harness_artifact_paths(temp.path());
        assert_eq!(paths.len(), 1);
        assert_eq!(
            read_sprint_contract_summary(temp.path()),
            Some("SprintContract: Scope: implement login endpoint. | Acceptance: POST /login returns JWT.".to_string())
        );
    }

    #[tokio::test]
    async fn writes_and_summarizes_outcome_verification() {
        let temp = tempdir().expect("tempdir");

        write_outcome_verification(
            temp.path(),
            "# Outcome Verification\n\nCommand: cargo nextest run\nResult: 12 passed, 0 failed\nBuild: cargo check PASSED\n",
        )
        .await
        .expect("write outcome verification");

        let paths = existing_harness_artifact_paths(temp.path());
        assert_eq!(paths.len(), 1);
        assert_eq!(
            read_outcome_verification_summary(temp.path()),
            Some(
                "OutcomeVerification: Command: cargo nextest run | Result: 12 passed, 0 failed | Build: cargo check PASSED"
                    .to_string()
            )
        );
    }

    #[tokio::test]
    async fn writes_and_summarizes_feature_list() {
        let temp = tempdir().expect("tempdir");

        write_feature_list(
            temp.path(),
            "# Feature List\n\n- [ ] Auth: login endpoint returns JWT\n- [x] API: health check endpoint\n",
        )
        .await
        .expect("write feature list");

        let paths = existing_harness_artifact_paths(temp.path());
        assert_eq!(paths.len(), 1);
        assert_eq!(
            read_feature_list_summary(temp.path()),
            Some("FeatureList: - [ ] Auth: login endpoint returns JWT | - [x] API: health check endpoint".to_string())
        );
    }

    #[tokio::test]
    async fn all_artifacts_counted_in_existing_paths() {
        let temp = tempdir().expect("tempdir");

        write_spec(temp.path(), "# Spec\ncontent\n").await.unwrap();
        write_contract(temp.path(), "# Contract\ncontent\n").await.unwrap();
        write_evaluation(temp.path(), "# Evaluation\ncontent\n").await.unwrap();
        write_sprint_contract(temp.path(), "# Sprint\ncontent\n").await.unwrap();
        write_outcome_verification(temp.path(), "# Outcome\ncontent\n").await.unwrap();
        write_feature_list(temp.path(), "# Features\ncontent\n").await.unwrap();

        let paths = existing_harness_artifact_paths(temp.path());
        assert_eq!(paths.len(), 6);
    }

    #[test]
    fn stale_spec_summary_is_dropped_for_later_sessions() {
        let temp = tempdir().expect("tempdir");
        let spec_path = current_spec_path(temp.path());
        fs::create_dir_all(spec_path.parent().expect("parent")).expect("tasks dir");
        fs::write(&spec_path, "# Execution Spec\nExplore the codebase and summarize.\n").expect("write spec");
        // Make the fixture look like a leftover from a prior session (days old).
        let old = SystemTime::now() - std::time::Duration::from_secs(48 * 60 * 60);
        let file = fs::File::options().write(true).open(&spec_path).expect("open");
        file.set_modified(old).expect("set mtime");

        assert!(read_spec_summary(temp.path()).is_some(), "unfiltered read still sees the file");
        assert!(
            read_spec_summary_fresh(temp.path(), Some(SystemTime::now())).is_none(),
            "a leftover fixture must not describe a later session"
        );
        assert!(
            read_spec_summary_fresh(temp.path(), Some(old + std::time::Duration::from_secs(10))).is_some(),
            "fresh reads still accept artifacts written during the session"
        );
    }

    #[test]
    fn handoff_artifact_written_just_before_session_start_stays_live() {
        let temp = tempdir().expect("tempdir");
        let spec_path = current_spec_path(temp.path());
        fs::create_dir_all(spec_path.parent().expect("parent")).expect("tasks dir");
        fs::write(&spec_path, "# Spec\n\nShip the residual hygiene fix.\n").expect("write spec");

        // Session directory is created *after* the handoff spec is written.
        let session_start = SystemTime::now();
        assert!(
            read_spec_summary_fresh(temp.path(), Some(session_start)).is_some(),
            "a just-written handoff artifact must survive the freshness cutoff"
        );
    }

    #[test]
    fn archive_completed_current_task_moves_only_fully_checked_trackers() {
        let temp = tempdir().expect("tempdir");
        let task_path = current_task_path(temp.path());
        fs::create_dir_all(task_path.parent().expect("parent")).expect("tasks dir");

        fs::write(&task_path, "# Work\n\n- [ ] open item\n- [x] done item\n").expect("write partial");
        assert!(
            archive_completed_current_task(temp.path(), "session-a")
                .expect("archive")
                .is_none(),
            "incomplete checklists stay live"
        );
        assert!(task_path.exists());

        fs::write(&task_path, "# Work\n\n- [x] done one\n- [x] done two\n").expect("write complete");
        let archived = archive_completed_current_task(temp.path(), "session-a")
            .expect("archive")
            .expect("fully-checked tracker is archived");
        assert!(!task_path.exists(), "live path must be clear for the next plan");
        assert!(archived.ends_with("current_task-session-a.md"));
        assert!(archived.exists());
    }
}