v0.1.0-rc.14
Pre-release
Pre-release
·
61 commits
to main
since this release
Added
applications interview show: surface the client-side contact block
(clientContactInfo) (#682). The captured AndroidInterviewdoc
carries the client'scontactFields(Slack id, email, phone, Skype),
but ttctl'sINTERVIEW_QUERYand projection trimmed them. The
client-side contact — distinct from the interviewer/recruiter-side
contacts[]— is now selected and projected onInterviewDetail, and
the CLI renders aClientsection afterContacts, omitted unless at
least one channel is populated. MCP:ttctl_applications_interview_show
auto-inherits the field (the tool JSON-serializes the full projection).
Wire-shape disposition: Schema/contract rule triggered (selection
extension on the hand-authoredInterviewop); Track 1
(packages/e2e/src/wire-snapshots/Interview.snapshot.jsonrefreshed —
the live run captured the field populated on an external interview).
Validated live (TTCTL_E2E=1) via
packages/e2e/src/62-applications-interview-show.e2e.test.ts.applications interview show: surface
contacts[*].topChatConversation(#683). The per-contact TopChat
discovery handle — selected by the captured AndroidInterview.graphql
but trimmed from ttctl, the per-contact twin of #682. Each
InterviewContactnow carriestopChatConversation(id,
slackChannelIdflattened from theTopChatConversationSlackService
inline fragment, anduploads[]withid/filename/url), and
the CLI renders a per-contactTopChat:block underContacts.
Discovery handle only — #23 owns the full TopChat surface (messages,
downloads). Wire-shape disposition: Schema/contract rule triggered
(the conversation/upload sub-shape is INFERRED from the captured doc;
the synthesized schema grounds only
TopChatConversationSlackService.channelId); Track 1 (Interview
snapshot refreshed). Validated live (TTCTL_E2E=1): the wire returned a
populated thread (idandslackChannelIdconfirmed as strings);
uploadswas empty on the live thread, so the upload-item shape stays
capture-inferred until a populated capture lands.timesheet show: surfaceTimesheetRecord.hoursandpersisted
(#684). Both fields are carried by the capturedTimesheetRecord
fragment but were trimmed from ttctl's ops and projection — the last
member of the #559 op-vs-projection spike batch (siblings #682 / #683).
Core:TimesheetRecordgainshours: string | null(server-rendered
hour string, sibling toduration) andpersisted: boolean | null
(save-state flag); both theTimesheetDetailsquery and the
SubmitTimesheetmutation select them, soshowandsubmitboth
surface them; MCP auto-inherits. CLI:timesheet showrenders the
serverhoursverbatim and derives fromdurationminutes only when
null — present-hours rows now render the server form (8.0h) instead
of the computed8.00h. Wire-shape disposition: Schema/contract rule
triggered (INFERRED fragment fields — the #275 duration-unit-bug
class); Track 1 (TimesheetDetailsandSubmitTimesheetsnapshots
refreshed, hand-preservingnote: nullable<string>against an all-null
cycle). Validated live (read-only, mobile gateway):hoursreturned as
a string andpersistedas a boolean across 15 records.- Interview read ops: sibling-reach footers (CLI) and tool-description
hints (MCP) (#694). The three interview read ops (interview show/
interview notes show/interview guide show) trim heavy job-context
cascades BY-DESIGN (#685) but never said where the full context lives.
Each pretty render now ends with a discovery footer naming the sibling
command that carries it (interview showpoints at
ttctl applications show <activityItemId>;notes show/guide show
point atttctl applications interview show <interviewId>), suppressed
when the target id is absent; the three MCP tool descriptions gain a
matching sentence.
Pretty-only —json/yamloutput is byte-unchanged.
availability-request showis deliberately excluded (it already renders
the job context inline). applications interview show: inlinejob.title(#696). Approach B
of #694 — a user could see an interview but not tell which job it was
for without a secondapplications showcall. Addstitleto the
Interviewop'sjobselection and renders a null-guardedTitle:
line in the CLIJobsection; the MCP payload carries the field
automatically. Re-evaluated per the #480 BY-DESIGN-to-OVERRIDE protocol:
a single-field override of the #685 job-cascade trim — the heavy
jobActivityItemDatacascade (roughly 50 fields) stays trimmed and
reachable viaapplications show <activityItemId>. Wire-shape
disposition: Schema/contract rule triggered (selectingtitle
directly oninterview.jobis a hand-authored selection extension);
Track 1 (Interview.snapshot.jsongainsjob.title: stringand
nothing else — verified surgically, not blind-regenerated). Validated
live (TTCTL_E2E=1, update and assert modes both 3/3) via e2e file 62;
job.titlereturned as a non-null string.
Changed
- TLS impersonation bumped to
chrome_147; the User-Agent now derives
from the profile (#38).node-wreqcatalog^2.2.1→^2.4.1(the
first release shipping achrome_147profile);IMPERSONATE_PROFILE
chrome_145→chrome_147.USER_AGENTnow derives its Chrome major
fromIMPERSONATE_PROFILEinstead of a second hardcoded literal, so the
UA and the TLS fingerprint can no longer drift — a profile bump rotates
the whole identity bundle. The photo-upload multipart path previously
hardcoded its ownChrome/145.0.0.0UA (a live cross-layer mismatch);
it now imports the shared constant. Verified live against the
Cloudflare-protectedtalent_profileportal (TTCTL_E2E=1, read-only
contracts file, 3/3) — noCf403Errorwith thechrome_147
fingerprint. Schema/contract rule NOT triggered (no wire-format
change; the tracked-path edit is a pure UA-constant refactor). applications availability-request show: theJobsection leads with
Title:(#699). Reorders the section to lead with the human-readable
title (thenJob id:/URL:/Client:), matching the Title-first
orderinterview showadopted in #696. Pretty-render only — field
content, alignment, and thejson/yamlprojection are unchanged.- Published tarballs drop compiled test fixtures and orphaned sourcemaps
(#701). Surfaced by the 0.1.0 release-readiness audit (CROSS-1).
@ttctl/coreshipped 20 compiled test-fixture files under
dist/__tests__/**, and every published package shipped.js.mapand
.d.ts.mapfiles referencing asrc/tree absent from the tarball
(orphaned — zero debugging value; 224 of@ttctl/cli's 452 files). A
per-packagetsconfig.build.json(build-only; the default
tsconfig.jsonstays untouched so type-aware ESLint keeps seeing the
fixtures) excludes**/__tests__/**and disablessourceMap/
declarationMap..d.tsdeclarations are preserved — consumers keep
full types.npm pack --dry-run: core 216 → 100 files, cli 452 → 228.
The sourcemap omission is a recorded, reversible 0.1.0 policy decision. THIRD-PARTY-NOTICES.mdships in all four published packages
(#705).node-wreqprebuilt binaries statically linkwreq
(Apache-2.0) and BoringSSL (Apache-2.0) plus a permissive Rust crate
graph, and upstream ships noLICENSE/NOTICEfiles in the binary
subpackages — as an AGPL redistributor, TTCtl inherits the
notice-preservation obligation. A rootTHIRD-PARTY-NOTICES.mdrecords
the verified licenses and is copied into every published tarball via
prepack(and listed infiles), alongsideLICENSE.
Fixed
profile.portfolio.add: strip update-onlytoptalRelatedfrom the
create wire (#645). The MCP add tool advertisedtoptalRelatedand
add()forwarded it onto the create wire, butPortfolioItemCreateInput
rejects the field (Field is not defined) — any add supplying it
failed. A live bogus-id probe settled the asymmetry: create REJECTS the
field while update ACCEPTS it, and on update the value is
server-controlled (supplyingtruereads backfalse, mirroring
Employment #402 / #508).add()now stripstoptalRelatedfrom the
create payload; the MCP add tool drops it from its input schema;
update()keeps it with a server-controlled doc note. The follow-up
secondary-field audit (#693) probed the remaining optional create fields
live and settled them as ACCEPTED (highlight,accomplishment,
clientOrCompanyName,websiteUrl) —toptalRelatedstays the only
rejected optional create field. (#693's interimhighlightstrip was
refuted by the live probe and reverted within this release window — no
net behavior change forhighlight.) Wire-shape disposition:
Schema/contract rule triggered; Track 1 (createPortfolioItem
snapshot unchanged). Validated live (TTCTL_E2E=1; 12/12 in the final
#693 run ofpackages/e2e/src/36-profile-portfolio.e2e.test.ts,
re-confirming thetoptalRelatedrejection and update round-trip).node-wreqnative-module load failure surfaces an actionable typed
error, and the supported-platform matrix is documented (#708). On
platforms wherenode-wreqships no prebuilt binary (linux-arm64-musl,
win32-arm64),npm i -g ttctlsucceeds — the binaries are optional
dependencies — and the FIRST Cloudflare-protected (talent-profile)
call threw a rawFailed to load native moduleerror while
mobile-gateway calls kept working. A newimpersonatedFetchwrapper
translates the load failure intoNativeModuleUnavailableError
(TtctlErrorsubclass, codeNATIVE_MODULE_UNAVAILABLE) naming the
live platform-arch pair, the supported set, and the two known gaps; all
three impersonatednode-wreqcall sites route through it, the stock
undicipath is untouched (mobile-gateway commands keep working), and
the README gains a supported-platforms matrix plus a drift-guard test
that fails loudly if a futurenode-wreqbump rewords the load-failure
messages the detector matches. Schema/contract rule NOT triggered
(pure error-handling wrapper; no wire-format change).surveys.submit: require mandatory answers client-side and model the
CHECKBOX value vocabulary (#754). A mandatoryINTERVIEW_ENDED
CHECKBOX question ("This interview didn't occur.") was unanswerable
through ttctl:surveys listsurfaced no value vocabulary for it
(answers: []), and omitting it was rejected opaquely server-side
((occurred): is not included in the list).prepareSubmissionnow
rejects unansweredisMandatoryquestions BEFORE any wire call, naming
each (id and label), andbuildSurveyAnswersvalidates an option-less
CHECKBOX value as"true"/"false"(case-insensitive in, lowercase
out,id: null) — the serialization the decompiled Android client uses
(String.valueOf(boolean)). The wire format needed no change; the gap
was vocabulary and completeness. CLI and MCP document the checkbox
vocabulary and surface each question'slabel. Wire-shape disposition:
Schema/contract rule triggered (inferred input contract); Track
1 (SubmitSurveyresponse snapshot unchanged). Live-confirmed
2026-06-12 via the gated positive path (TTCTL_E2E_SUBMIT_SURVEY) — a
realINTERVIEW_ENDEDround-trip confirmed the unchecked-maps-to-
"false"-maps-to-occurredcontract.
Security
- MCP file-upload sandbox resolves symlinks before the prefix check,
closing an exfiltration bypass (#707). The path-prefix sandbox used
path.resolve— logical..normalization only — so a symlink staged
inside the sandbox pointing out (~/Documents/innocent.pdfto
~/.ssh/id_rsa) with an allowed extension passed both defense-in-depth
gates, and the upload path would read the link TARGET — arbitrary-file
exfiltration to the operator's Toptal profile on a successful prompt
injection (audit ref: security M1).validateSandboxnow resolves the
real on-disk location withfs.realpathSync(final component AND
intermediate-dir symlinks) before the prefix check and refuses when the
real location is outside~/Documents/~/Downloads/~/Desktop—
refused, not silently followed; a symlink whose real target stays inside
the sandbox is still accepted. On a realpath error the gate falls back
to the lexical path (a pathrealpathcannot resolve is onereadFile
cannot read either). TheTTCTL_MCP_FILE_UPLOAD_ALLOW_ANY=1bypass and
the extension allowlist are unchanged. Unit-level TDD: the bypass tests
were RED against the pre-fix gate, GREEN after.
Dependencies
- Bump
commander14.0.3 → 15.0.0 (#713),@clack/prompts1.4.0 → 1.5.1
(#714),node-wreq^2.2.1 → ^2.4.1 (#681, thechrome_147bump above),
turbo2.9.14 → 2.9.16 (#715),tsx4.22.3 → 4.22.4 (#716),eslint
10.4.0 → 10.4.1 (#717),typescript-eslint8.59.4 → 8.60.1 (#718),
@graphql-codegen/add7.0.0 → 7.0.1 (#719),@graphql-codegen/cli
7.0.0 → 7.1.2 (#712),vitestand@vitest/coverage-v84.1.5 → 4.1.8
(#720, #721),actions/checkout6.0.2 → 6.0.3 (#711). - Override transitive
shell-quoteto^1.8.4(GHSA-w7jw-789q-3m8p,
critical, reachable via@graphql-codegen/cli). The release-time
pnpm audit --audit-level=highgate flagged the freshly published
advisory and blocked the first rc.14 publish attempt; the override
re-resolves the locked 1.8.3 to the patched line.