Skip to content

fix(skills): track real phase timestamps, open GitHub issues, enforce batch size#702

Merged
carlos-alm merged 10 commits intomainfrom
fix/titan-phase-timestamps
Mar 30, 2026
Merged

fix(skills): track real phase timestamps, open GitHub issues, enforce batch size#702
carlos-alm merged 10 commits intomainfrom
fix/titan-phase-timestamps

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

  • titan-run: Records phaseTimestamps.{phase}.startedAt/completedAt in titan-state.json at each phase boundary, giving CLOSE real wall-clock data instead of fabricated timestamps
  • titan-close: Reads phaseTimestamps from state for the Pipeline Timeline table; falls back to git log timestamps when missing. Opens GitHub issues (gh issue create) for bug/limitation severity entries instead of only listing them in the report
  • titan-recon: Enforces hard limit of 5 files per batch (was ~5-15). titan-run V4 validation now warns on oversized batches

Motivation

The v3.5.0 Titan run produced a report with completely fabricated phase durations (~30 hrs for gauntlet, ~14 hrs for forge) when the actual pipeline took ~3.5 hrs. The CLOSE sub-agent had no access to real timing data and guessed.

Test plan

  • Run /titan-run on a small target and verify phaseTimestamps appears in titan-state.json
  • Verify the generated report has accurate durations matching wall-clock time
  • Verify GitHub issues are created for bug/limitation entries
  • Verify recon produces batches with <= 5 files each

… batch size

titan-run: record phaseTimestamps.{phase}.startedAt/completedAt in
titan-state.json at each phase boundary so CLOSE has real wall-clock data.

titan-close: read phaseTimestamps from state instead of guessing; fall back
to git log timestamps when missing. Open GitHub issues (gh issue create)
for bug/limitation severity entries instead of only listing them in the report.

titan-recon: enforce hard limit of 5 files per batch to prevent context
overload in gauntlet sub-agents. titan-run V4 now warns on oversized batches.
@claude
Copy link
Copy Markdown

claude bot commented Mar 30, 2026

Claude finished @carlos-alm's task —— View job


PR Review Complete

Todo List

  • Phase 1: Root cause analysis - understand the fabricated timestamp problem
  • Phase 2: Configuration change detection - check for rule relaxation
  • Phase 3: Test integrity - verify no weakened assertions
  • Phase 4: Code quality review - analyze the skill file changes
  • Phase 5: Backlog compliance assessment
  • Final recommendation with star rating

Phase 1: Root Cause Analysis ✅

Problem Being Solved: The v3.5.0 Titan run produced fabricated phase durations (~30 hrs for gauntlet, ~14 hrs for forge) when the actual pipeline took ~3.5 hrs. The CLOSE sub-agent had no access to real timing data and invented timestamps.

Why This Approach: Records real wall-clock timestamps at phase boundaries in titan-state.json using Node.js new Date().toISOString(). CLOSE phase reads these actual timestamps instead of guessing.

Risk Assessment: This is fixing a real data accuracy issue, not masking symptoms. The fabricated timestamps made reports meaningless for duration analysis.

Phase 2: Configuration Change Detection ✅

No HIGH-RISK changes detected:

  • ✅ No Biome/linting rules disabled or relaxed
  • ✅ No test timeouts increased or coverage thresholds lowered
  • ✅ No build configuration or module resolution modified
  • ✅ No TypeScript strict mode flags disabled
  • ✅ Changes are purely additive documentation/process improvements

Phase 3: Test Integrity ✅

No RED FLAGS detected:

  • ✅ No test files modified
  • ✅ No assertions removed or weakened
  • ✅ No test expectations lowered to make tests pass
  • ✅ No eslint-disable or workaround comments added
  • ✅ Changes are confined to skill documentation files only

Phase 4: Code Quality Review ✅

Changes analyzed across 3 skill files:

titan-run/SKILL.md (+36/-0):

  • Good addition: Real timestamp recording via Node.js new Date().toISOString()
  • Clean implementation: Helper commands for recording startedAt/completedAt at phase boundaries
  • Proper fallback logic: Only sets startedAt if not already present (handles resume scenarios)
  • Clear documentation: Step-by-step instructions with exact command templates

titan-close/SKILL.md (+47/-8):

  • Key improvement: Pipeline Timeline section now reads from phaseTimestamps in titan-state.json
  • Robust fallback: Falls back to git log timestamps when phaseTimestamps is missing
  • Enhanced functionality: Added GitHub issue creation for bug/limitation severity entries using gh issue create
  • Well-structured: Clear delineation between real timestamp usage vs fallback methods
  • ⚠️ Minor: Could benefit from error handling around gh command availability

titan-recon/SKILL.md (+2/-1):

  • Focused change: Enforces hard limit of 5 files per batch (was ~5-15)
  • Addresses context overload: Prevents gauntlet sub-agents from being overwhelmed
  • Clear validation: V4 check warns on oversized batches during titan-run

Overall code quality: HIGH

  • Changes address the actual root cause (lack of real timing data)
  • Implementation is clean and well-documented
  • Maintains backward compatibility with fallback logic
  • No technical debt introduced

Phase 5: Backlog Compliance Assessment ✅

Backlog Compliance

  • Zero-dep: ✓ — Uses only Node.js built-ins (new Date().toISOString()) and existing git/gh CLI tools
  • Foundation-aligned: ✓ — Improves data accuracy for Titan reports, aligns with P3 (confidence over noise)
  • Problem-fit: 4/5 — Directly improves AI agent accuracy by providing real duration data instead of fabricated timestamps
  • Breaking: No — Changes are purely additive to skill documentation
  • Tier: 1

Critical Concerns

None identified. This is a solid improvement that fixes a data accuracy issue.

Minor Suggestions

  1. titan-close/SKILL.md line 254: Consider adding error handling for gh command availability
  2. Validation enhancement: Could add timestamp validation to ensure startedAt < completedAt

Final Assessment

## Root Cause Analysis
**Problem Being Solved**: v3.5.0 Titan reports showed completely fabricated phase durations (~30 hrs gauntlet, ~14 hrs forge) when actual pipeline took ~3.5 hrs. CLOSE sub-agent had no real timing data.
**Why This Approach**: Records real wall-clock timestamps at phase boundaries in titan-state.json; CLOSE reads actual data instead of inventing it.
**Risk Assessment**: Low risk. Fixes genuine data accuracy issue with clean implementation and proper fallbacks.

## Backlog Compliance
- Zero-dep: ✓ — uses Node.js built-ins + existing CLI tools
- Foundation-aligned: ✓ — improves data accuracy (P3: confidence over noise)
- Problem-fit: 4 — directly improves AI agent accuracy with real duration data
- Breaking: No — purely additive documentation changes
- Tier: 1

## Critical Concerns
None. Clean implementation addressing real data quality issue.

## Final Recommendation
- Rating: ⭐⭐⭐⭐☆ (4/5)  
- Action: APPROVE
- Reasoning: Solid fix for fabricated timestamp issue. Clean implementation, good documentation, proper fallbacks, addresses real accuracy problem. Minor deduction for lacking error handling around gh command, but overall excellent quality.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR fixes the fabricated-timestamp problem from the v3.5.0 Titan run by recording real wall-clock phase boundaries in titan-state.json, opening GitHub issues for bug/limitation severity entries via gh issue create, and enforcing a hard 5-file batch limit in titan-recon.

Key changes:

  • titan-run: Adds resume-safe node one-liners to record phaseTimestamps.{phase}.startedAt/completedAt at every phase boundary; the startedAt guard correctly uses an existence check so crash/resume cycles don't clobber the original timestamp. The recon stub-creation variant handles the ENOENT case on fresh runs.
  • titan-close: Adds Step 5b GitHub issue creation using --body-file (temp file pattern avoids heredoc quoting hazards); deduplication via gh issue list --search; titan-audit label auto-created on demand. Step 7 now records close.completedAt before writing the report, giving the Pipeline Timeline accurate CLOSE duration data.
  • titan-recon: Hard batch limit changed from "~5-15" to "≤ 5 files", with a corresponding V4 validation warning in titan-run.
  • One remaining issue: The phaseTimestamps field in the titan-recon Step 12 JSON template uses a string placeholder (\"<merged from existing file — see above>\"). Since all other placeholders in the same template are also strings, an AI agent following the pattern could write this literal string into titan-state.json. A non-empty string is truthy, so the ||{} guard in every downstream one-liner silently fails, and the very next property access throws TypeError. The placeholder should be {} (an object literal).

Confidence Score: 4/5

Safe to merge after fixing the string placeholder in the titan-recon JSON template — as written it will silently break all post-recon timestamp recording for the exact failure mode this PR was designed to prevent.

All previous review concerns are resolved. One new P1 remains: the phaseTimestamps JSON template placeholder is a string where an object is required, which causes downstream TypeError in every phase's timestamp one-liner. Fixing it is a one-line change.

.claude/skills/titan-recon/SKILL.md line 293 — phaseTimestamps placeholder type mismatch.

Important Files Changed

Filename Overview
.claude/skills/titan-run/SKILL.md Adds phase timestamp helper (start/completion node one-liners), resume-safe startedAt guard, validation with process.exit(0) on clock-skew, ENOENT-safe recon stub creation, and batch-size V4 warning. All previous review concerns have been addressed.
.claude/skills/titan-close/SKILL.md Adds Step 5b (GitHub issue creation via --body-file temp file, dedup check, titan-audit label), Step 7 close.completedAt pre-report recording, and a redesigned Pipeline Timeline table backed by real phaseTimestamps. All previously raised issues resolved.
.claude/skills/titan-recon/SKILL.md Enforces 5-file hard batch limit and adds phaseTimestamps merge in Step 12. However, the JSON template uses a string placeholder where an object is required — an AI following the template literally would write a string value, making all subsequent timestamp node one-liners throw TypeError.

Sequence Diagram

sequenceDiagram
    participant OR as titan-run (orchestrator)
    participant TS as titan-state.json
    participant RE as titan-recon (sub-agent)
    participant CL as titan-close (sub-agent)
    participant GH as GitHub API

    OR->>TS: write recon.startedAt (ENOENT-safe stub)
    OR->>RE: dispatch sub-agent
    RE->>TS: Step 12: read existing phaseTimestamps, merge into full state write
    RE-->>OR: return
    OR->>TS: write recon.completedAt
    OR->>TS: write gauntlet.startedAt (only if absent)
    note over OR,TS: gauntlet loop
    OR->>TS: write gauntlet.completedAt
    OR->>TS: write sync.startedAt
    note over OR,TS: sync sub-agent
    OR->>TS: write sync.completedAt
    OR->>TS: write forge.startedAt (only if absent)
    note over OR,TS: forge loop
    OR->>TS: write forge.completedAt
    OR->>TS: write close.startedAt
    OR->>CL: dispatch sub-agent
    CL->>TS: Step 7: write close.completedAt (before report)
    CL->>GH: gh issue create (bug/limitation severity)
    GH-->>CL: issue #N
    CL->>CL: write titan-report-*.md (Pipeline Timeline from phaseTimestamps)
    CL-->>OR: return
    OR->>TS: write close.completedAt (safety backstop)
Loading

Reviews (5): Last reviewed commit: "fix(skills): handle recon.startedAt on f..." | Re-trigger Greptile


```bash
# Record phase start:
node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('.codegraph/titan/titan-state.json','utf8'));s.phaseTimestamps=s.phaseTimestamps||{};s.phaseTimestamps['<PHASE>']=s.phaseTimestamps['<PHASE>']||{};s.phaseTimestamps['<PHASE>'].startedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 startedAt always overwritten on resume

The provided node one-liner unconditionally assigns startedAt = new Date().toISOString() regardless of whether a value already exists. The very next line of prose correctly states "do NOT overwrite startedAt — only set it if it doesn't already exist," but the command doesn't implement that guard.

On a gauntlet or forge resume:

  1. gauntlet.startedAt was recorded at the original start (T1)
  2. Pipeline crashes mid-way
  3. Orchestrator resumes and re-runs the start command
  4. gauntlet.startedAt is silently clobbered with the resume time (T2)
  5. The reported duration is now artificially short — exactly the fabricated-timestamp problem this PR was written to fix

The fix is to add an existence check before writing:

# Record phase start (safe for resume):
node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('.codegraph/titan/titan-state.json','utf8'));s.phaseTimestamps=s.phaseTimestamps||{};s.phaseTimestamps['<PHASE>']=s.phaseTimestamps['<PHASE>']||{};if(!s.phaseTimestamps['<PHASE>'].startedAt){s.phaseTimestamps['<PHASE>'].startedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));}"

Without this guard, the main invariant of the PR — accurate wall-clock durations across crashes and resumes — is broken for multi-iteration phases (gauntlet, forge).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- added the \ guard to the start timestamp command. The one-liner now only writes \ when it doesn't already exist, preserving the original timestamp across crash/resume cycles. The prose and the code are now consistent.

- **Label:** Use `titan-audit` label. If the label doesn't exist, create it: `gh label create titan-audit --description "Issues discovered during Titan audit" --color "d4c5f9" 2>/dev/null || true`
- **Record each created issue number** for inclusion in the report's Issues section.

For `suggestion` and `codebase` severity entries, include them in the report's Issues section but do NOT create GitHub issues.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 codebase is a category, not a severity

The sentence mixes up the two orthogonal schema fields. Looking at the NDJSON schema defined just above:

{"severity": "bug|limitation|suggestion", "category": "codegraph|tooling|process|codebase", ...}

codebase is a category value, not a severity value. The sentence currently reads:

For suggestion and codebase severity entries…

This creates ambiguity: an AI agent strictly following the severity field will never find a "severity": "codebase" entry (it doesn't exist), so this rule effectively becomes a no-op. Meanwhile, entries with category: "codebase" that carry severity: "bug" or severity: "limitation" will still trigger GitHub issue creation — which may or may not be the intended behaviour.

If the intent is to suppress GitHub issues for all category: "codebase" entries regardless of severity, the rule should read:

Suggested change
For `suggestion` and `codebase` severity entries, include them in the report's Issues section but do NOT create GitHub issues.
For `suggestion` severity entries and entries with `category: "codebase"`, include them in the report's Issues section but do NOT create GitHub issues.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- updated the sentence to correctly distinguish between severity and category. Now reads: "For \ severity entries and entries with , include them in the report's Issues section but do NOT create GitHub issues." This makes the filter rule unambiguous for AI agents.

Comment on lines +238 to +253
gh issue create --title "<category>: <short description>" --body "$(cat <<'EOF'
## Context
Discovered during Titan audit (phase: <phase>, date: <timestamp>).

## Description
<description>

## Additional Context
<context field, if present>

## Source
- **Titan phase:** <phase>
- **Severity:** <severity>
- **Category:** <category>
EOF
)" --label "titan-audit"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Heredoc in subshell is fragile with special characters

The --body "$(cat <<'EOF'...EOF)" pattern has two edge cases worth guarding against:

  1. Literal EOF on its own line in the <description> or <context> field will prematurely terminate the heredoc and produce a malformed or partially-executed command. Issue descriptions scraped from code comments or stack traces can contain this.

  2. Double-quotes inside the description/context will not break the heredoc itself (because 'EOF' suppresses expansion), but the outer "$(…)" argument to --body can still be affected by backticks or $( ) sequences that appear verbatim in the inserted text.

A safer pattern is to write the body to a temp file and pass it via --body-file:

BODY=$(mktemp)
cat > "$BODY" <<'EOF'
## Context
...
EOF
gh issue create --title "..." --body-file "$BODY" --label "titan-audit"
rm -f "$BODY"

This avoids quoting/expansion issues entirely and is easier to read.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- replaced the heredoc-in-subshell pattern with a --body-file approach using a temp file. Also changed the heredoc delimiter from EOF to ISSUE_BODY to avoid the premature termination risk if issue descriptions contain literal EOF lines. Added an explanatory note about why this pattern is preferred.

Add gh CLI pre-check in titan-close before attempting GitHub issue
creation, with graceful fallback when gh is unavailable or
unauthenticated. Add startedAt < completedAt timestamp validation
in titan-run after recording phase completion timestamps.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed both minor suggestions from the automated review:

  1. gh command availability (titan-close): Added a gh auth status pre-check before attempting GitHub issue creation in Step 5b. If gh is unavailable or unauthenticated, issue creation is skipped gracefully with a note in the report directing the user to create them manually.

  2. Timestamp validation (titan-run): Added a validation step after recording completedAt for any phase. It verifies startedAt < completedAt and logs the phase duration. If the check fails (e.g., clock skew), it warns but does not stop the pipeline.

- Guard startedAt against overwrite on resume in titan-run timestamp
  helper (prevents fabricated short durations after crash/resume)
- Fix "codebase" referenced as severity instead of category in
  titan-close issue creation rules
- Replace fragile heredoc-in-subshell pattern with --body-file temp
  file for gh issue create to avoid quoting/expansion edge cases
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@claude

@claude
Copy link
Copy Markdown

claude bot commented Mar 30, 2026

Claude finished @carlos-alm's task —— View job


Claude is analyzing the updated PR

Todo List

  • Read current state of all modified files
  • Verify fixes for previously identified issues
  • Assess overall implementation quality
  • Provide comprehensive review feedback

Analysis Summary

I've analyzed the current state of the PR after your fixes and verified the implementation against previous review feedback.

Previous Issues Successfully Addressed

  1. startedAt overwrite bug (titan-run/SKILL.md:47): ✅ FIXED

    • The Node.js one-liner now includes proper existence checking: if(!s.phaseTimestamps['<PHASE>'].startedAt)
    • This prevents overwriting timestamps on resume, preserving accurate wall-clock durations
  2. Terminology confusion (titan-close/SKILL.md:272): ✅ FIXED

    • Sentence now correctly reads: "For suggestion severity entries and entries with category: "codebase", include them in the report's Issues section but do NOT create GitHub issues"
    • Clearly distinguishes between severity and category fields
  3. Heredoc fragility (titan-close/SKILL.md:244-261): ✅ FIXED

    • Replaced --body "$(cat <<'EOF'...EOF)" pattern with safer --body-file approach using temp files
    • Changed delimiter from EOF to ISSUE_BODY to avoid premature termination
    • Added explanatory comment about why this pattern is preferred

Additional Quality Improvements

  1. GitHub CLI availability check (titan-close/SKILL.md:235-239): ✅ EXCELLENT ADDITION

    • Added gh auth status pre-check before attempting issue creation
    • Graceful fallback with clear user guidance when gh is unavailable
  2. Timestamp validation (titan-run/SKILL.md:55-59): ✅ EXCELLENT ADDITION

    • Added validation step to verify startedAt < completedAt with helpful duration logging
    • Warns on clock skew but doesn't stop pipeline, which is the right approach

🔍 Code Quality Assessment

Overall Implementation Quality: EXCELLENT

  • Root cause addressed: Fixes the actual problem of fabricated timestamps vs real wall-clock data
  • Robust error handling: Added proper guards, fallbacks, and validation
  • Clear documentation: Instructions are precise and include exact command templates
  • Backward compatibility: All changes are purely additive
  • Security considerations: Uses temp files and proper shell escaping

🎯 Technical Review

titan-run/SKILL.md (+42/-0):

  • ✅ Timestamp recording logic is now bulletproof
  • ✅ Resume scenarios properly handled
  • ✅ Validation prevents clock skew issues
  • ✅ Clear command templates with exact syntax

titan-close/SKILL.md (+57/-8):

  • ✅ Pipeline Timeline section now reads real phaseTimestamps data
  • ✅ Robust fallback to git timestamps when needed
  • ✅ GitHub issue creation with proper error handling
  • ✅ Shell-safe --body-file pattern implemented
  • ✅ Terminology correctly distinguishes severity vs category

titan-recon/SKILL.md (+2/-1):

  • ✅ Batch size enforcement tightened to exactly 5 files
  • ✅ Simple, focused change addressing context overload

📊 Final Assessment

## Status: READY TO MERGE ✅

### Code Quality: ⭐⭐⭐⭐⭐ (5/5)
- All critical issues from previous reviews have been resolved
- Implementation is clean, robust, and well-documented  
- Added meaningful improvements beyond the original fixes
- No technical debt introduced

### Problem-Solution Fit: 4.5/5
- Directly solves fabricated timestamp problem with real wall-clock data
- Enhanced with validation and error handling
- Addresses context overload with batch size limits

### Risk Assessment: LOW
- Changes are confined to documentation/instruction files
- No breaking changes to core logic
- Proper fallbacks and error handling implemented

### Recommendation: APPROVE AND MERGE
This PR represents a solid improvement that fixes genuine data accuracy issues while adding valuable defensive programming practices.

| SYNC | <computed from phaseTimestamps.sync> | — |
| FORGE | <computed from phaseTimestamps.forge> | <commit count>, first at <time>, last at <time> |
| GATE | across forge | <total runs> inline with forge commits |
| CLOSE | <computed from phaseTimestamps.close> | — |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 close.completedAt is never available when the report is generated

phaseTimestamps.close.completedAt is written by titan-run after titan-close returns (see titan-run/SKILL.md Step 5c). When titan-close is building the Pipeline Timeline table, completedAt for the CLOSE phase does not yet exist in titan-state.json — only startedAt is present.

The template on this line says <computed from phaseTimestamps.close>, but with a missing completedAt an AI agent following these instructions has no real value to compute from. This is the same "fabricated timestamps" failure mode the PR was written to fix: an agent asked to compute a duration it cannot derive will either guess or use a placeholder.

The simplest fix is to have titan-close record its own completedAt immediately before writing the final report, then use that value in the table:

# Just before writing the final report file:
node -e "const fs=require('fs');const s=JSON.parse(fs.readFileSync('.codegraph/titan/titan-state.json','utf8'));s.phaseTimestamps=s.phaseTimestamps||{};s.phaseTimestamps['close']=s.phaseTimestamps['close']||{};s.phaseTimestamps['close'].completedAt=new Date().toISOString();fs.writeFileSync('.codegraph/titan/titan-state.json',JSON.stringify(s,null,2));"

Then titan-run's existing "Record phaseTimestamps.close.completedAt" step (Step 5c) can be kept as a safety backstop or removed to avoid overwriting the more-accurate in-close value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- added a "Record CLOSE completion timestamp" step in titan-close Step 7, immediately before writing the report. titan-close now records its own phaseTimestamps.close.completedAt so the Pipeline Timeline CLOSE row has accurate duration data. The existing titan-run Step 5c recording is preserved as a safety backstop but the report no longer depends on it.

titan-run records phaseTimestamps.close.completedAt after titan-close
returns, but by then the report is already written. Add an instruction
for titan-close to record its own completedAt just before generating
the report, so the Pipeline Timeline CLOSE row has accurate data.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

Comment on lines +57 to +59
node -e "const s=JSON.parse(require('fs').readFileSync('.codegraph/titan/titan-state.json','utf8'));const p=s.phaseTimestamps?.['<PHASE>'];if(p?.startedAt&&p?.completedAt){const start=new Date(p.startedAt),end=new Date(p.completedAt);if(end<=start){console.log('WARNING: <PHASE> completedAt ('+p.completedAt+') is not after startedAt ('+p.startedAt+')');process.exit(1);}console.log('<PHASE> duration: '+((end-start)/60000).toFixed(1)+' min');}else{console.log('WARNING: <PHASE> missing startedAt or completedAt');}"
```
If the check fails, log a warning but do not stop the pipeline — clock skew or immediate completion of short phases can cause this.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 process.exit(1) contradicts "do not stop the pipeline"

The validation script calls process.exit(1) when end <= start, but the prose immediately below says "If the check fails, log a warning but do not stop the pipeline." These two instructions directly contradict each other.

When an AI agent runs this node command and gets back exit code 1, it has no way to distinguish "this is a warning that should be ignored" from "this is a hard error that should abort the current step." Depending on the agent's default behavior for non-zero exit codes, it may abort the current phase — which is exactly the kind of spurious pipeline stall the PR is designed to prevent (the v3.5.0 fabricated-timestamp issue was partly caused by a crashed pipeline).

Since the intent is clearly to warn and continue, the process.exit(1) should be replaced with process.exit(0):

Suggested change
node -e "const s=JSON.parse(require('fs').readFileSync('.codegraph/titan/titan-state.json','utf8'));const p=s.phaseTimestamps?.['<PHASE>'];if(p?.startedAt&&p?.completedAt){const start=new Date(p.startedAt),end=new Date(p.completedAt);if(end<=start){console.log('WARNING: <PHASE> completedAt ('+p.completedAt+') is not after startedAt ('+p.startedAt+')');process.exit(1);}console.log('<PHASE> duration: '+((end-start)/60000).toFixed(1)+' min');}else{console.log('WARNING: <PHASE> missing startedAt or completedAt');}"
```
If the check fails, log a warning but do not stop the pipeline — clock skew or immediate completion of short phases can cause this.
node -e "const s=JSON.parse(require('fs').readFileSync('.codegraph/titan/titan-state.json','utf8'));const p=s.phaseTimestamps?.['<PHASE>'];if(p?.startedAt&&p?.completedAt){const start=new Date(p.startedAt),end=new Date(p.completedAt);if(end<=start){console.log('WARNING: <PHASE> completedAt ('+p.completedAt+') is not after startedAt ('+p.startedAt+')');process.exit(0);}console.log('<PHASE> duration: '+((end-start)/60000).toFixed(1)+' min');}else{console.log('WARNING: <PHASE> missing startedAt or completedAt');}"

If the check fails, log a warning but do not stop the pipeline — clock skew or immediate completion of short phases can cause this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- changed process.exit(1) to process.exit(0) in the timestamp validation one-liner. The warning is still printed to stdout, but the exit code no longer signals a hard error, which is consistent with the prose instruction to not stop the pipeline on clock skew.

The timestamp validation script used process.exit(1) on clock skew,
contradicting the prose instruction to "log a warning but do not stop
the pipeline." An AI agent seeing exit code 1 may abort the phase.
Changed to process.exit(0) so the warning is non-fatal.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

Comment on lines +149 to +150
### 1a.1. Record phase start timestamp
Record `phaseTimestamps.recon.startedAt` (only if not already set — it may exist from a prior crashed run).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 recon.startedAt always fails on fresh runs — ENOENT

Step 1a.1 instructs the orchestrator to record phaseTimestamps.recon.startedAt by running the helper node one-liner, but on a fresh run titan-state.json does not yet exist at this point. The file is only created at the very end of the recon sub-agent — in titan-recon Step 12. The node command will throw:

Error: ENOENT: no such file or directory, open '.codegraph/titan/titan-state.json'

Furthermore, even if the orchestrator pre-created a stub file, titan-recon Step 12 overwrites it wholesale with a freshly built JSON object that has no phaseTimestamps key, so any pre-written startedAt would be lost regardless.

The end result: on every fresh run phaseTimestamps.recon.startedAt is never written (only completedAt is recorded after validation), the Pipeline Timeline falls back to approximate git-log timestamps for the RECON row — exactly the fabricated-timestamp problem this PR was written to fix.

Recommended fix: Add phaseTimestamps to the initial state template in titan-recon Step 12, seeding recon.startedAt at the moment the sub-agent begins executing:

{
  "version": 1,
  "initialized": "<ISO 8601>",
  ...
  "phaseTimestamps": {
    "recon": {
      "startedAt": "<ISO 8601 — set at start of Step 1>"
    }
  }
}

Alternatively, have titan-recon record its own startedAt as the very first action (before any heavy processing), writing directly into a field it then embeds in the Step 12 write.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- addressed the ENOENT issue in two places:

  1. titan-run Step 1a.1: Added a safe variant of the timestamp helper that catches the missing file and creates a minimal stub with phaseTimestamps.recon.startedAt when titan-state.json doesn't exist yet. Uses try/catch around the readFileSync and falls back to mkdirSync + empty object.

  2. titan-recon Step 12: Added instructions to read any existing phaseTimestamps from the file before overwriting it, and merge them into the full state object. The JSON template now includes a phaseTimestamps field that must be populated from the existing file.

This ensures recon.startedAt is recorded on fresh runs and survives the Step 12 wholesale write.

Regarding the titan-audit label concern from the summary: this is already handled in titan-close Step 5b's rules section -- gh label create titan-audit ... 2>/dev/null || true runs before the first gh issue create, so the label is created on-demand if missing.

The recon start timestamp helper threw ENOENT on fresh runs because
titan-state.json doesn't exist until titan-recon Step 12. The fix adds
a safe variant that creates a minimal stub when the file is missing and
updates titan-recon to merge any existing phaseTimestamps into the full
state file it writes.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit 082a954 into main Mar 30, 2026
12 checks passed
@carlos-alm carlos-alm deleted the fix/titan-phase-timestamps branch March 30, 2026 12:13
@github-actions github-actions bot locked and limited conversation to collaborators Mar 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant